mirror of
https://github.com/hathach/tinyusb.git
synced 2026-08-18 11:02:16 +00:00
Merge pull request #3670 from hathach/update-labeler
ci: sponsor/Adafruit labeling + Sponsor Triage board sync
This commit is contained in:
135
.github/workflows/labeler.yml
vendored
135
.github/workflows/labeler.yml
vendored
@ -5,6 +5,8 @@ on:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
discussion:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
label-priority:
|
||||
@ -12,15 +14,17 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
steps:
|
||||
- name: Label New Issue or PR
|
||||
- name: Label New Issue, PR or Discussion
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
let label = '';
|
||||
let labels = [];
|
||||
let username = '';
|
||||
let issueOrPrNumber = 0;
|
||||
let discussionNodeId = '';
|
||||
|
||||
if (context.eventName === 'issues') {
|
||||
username = context.payload.issue.user.login;
|
||||
@ -28,25 +32,85 @@ jobs:
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
username = context.payload.pull_request.user.login;
|
||||
issueOrPrNumber = context.payload.pull_request.number;
|
||||
} else if (context.eventName === 'discussion') {
|
||||
username = context.payload.discussion.user.login;
|
||||
discussionNodeId = context.payload.discussion.node_id;
|
||||
}
|
||||
|
||||
// Check if an Adafruit member
|
||||
try {
|
||||
const adafruitResponse = await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'adafruit',
|
||||
username: username
|
||||
});
|
||||
// Maintainer is an Adafruit member; skip the Adafruit perks for their own
|
||||
// issues/PRs and treat them as a plain contributor (Prio only).
|
||||
const isOwner = username.toLowerCase() === 'hathach';
|
||||
|
||||
if (adafruitResponse.status === 204) {
|
||||
console.log('Adafruit Member');
|
||||
label = 'Prio Urgent';
|
||||
// Check if an Adafruit member: Adafruit + Sponsor + top priority
|
||||
if (!isOwner) {
|
||||
try {
|
||||
const adafruitResponse = await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'adafruit',
|
||||
username: username
|
||||
});
|
||||
|
||||
if (adafruitResponse.status === 204) {
|
||||
console.log('Adafruit Member');
|
||||
labels = ['Adafruit', 'Sponsor', 'Prio Top'];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Not an Adafruit member');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Not an Adafruit member');
|
||||
}
|
||||
|
||||
// Check if a contributor
|
||||
if (label == '') {
|
||||
// Check if a public GitHub Sponsor of the repo owner.
|
||||
// Word ($32) tier and up get triage priority; DWORD/QWORD ($128+) go to the top.
|
||||
// Private sponsorships are not visible to GITHUB_TOKEN, so only public sponsors are detected.
|
||||
if (labels.length === 0) {
|
||||
try {
|
||||
const result = await github.graphql(`
|
||||
query($sponsorable: String!, $sponsor: String!) {
|
||||
user(login: $sponsorable) {
|
||||
isSponsoredBy(accountLogin: $sponsor)
|
||||
sponsorshipsAsMaintainer(includePrivate: false, first: 100) {
|
||||
nodes {
|
||||
sponsorEntity {
|
||||
... on User { login }
|
||||
... on Organization { login }
|
||||
}
|
||||
tier { monthlyPriceInDollars }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, { sponsorable: context.repo.owner, sponsor: username });
|
||||
|
||||
const owner = result.user;
|
||||
if (owner && owner.isSponsoredBy) {
|
||||
let monthly = 0;
|
||||
const nodes = (owner.sponsorshipsAsMaintainer && owner.sponsorshipsAsMaintainer.nodes) || [];
|
||||
for (const node of nodes) {
|
||||
const login = node.sponsorEntity && node.sponsorEntity.login;
|
||||
if (login && login.toLowerCase() === username.toLowerCase()) {
|
||||
monthly = (node.tier && node.tier.monthlyPriceInDollars) || 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (monthly >= 128) {
|
||||
console.log('Sponsor (DWORD/QWORD tier)');
|
||||
labels = ['Sponsor', 'Prio Top'];
|
||||
} else if (monthly >= 32) {
|
||||
console.log('Sponsor (Word tier)');
|
||||
labels = ['Sponsor', 'Prio'];
|
||||
} else {
|
||||
console.log('Sponsor (below Word tier or tier not visible)');
|
||||
labels = ['Sponsor'];
|
||||
}
|
||||
} else {
|
||||
console.log('Not a public sponsor');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Sponsor lookup failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a contributor: prioritized in triage queue
|
||||
if (labels.length === 0) {
|
||||
try {
|
||||
const collaboratorResponse = await github.rest.repos.checkCollaborator({
|
||||
owner: context.repo.owner,
|
||||
@ -56,18 +120,43 @@ jobs:
|
||||
|
||||
if (collaboratorResponse.status === 204) {
|
||||
console.log('Contributor');
|
||||
label = 'Prio Higher';
|
||||
labels = ['Prio'];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Not a contributor');
|
||||
}
|
||||
}
|
||||
|
||||
if (label !== '') {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueOrPrNumber,
|
||||
labels: [label]
|
||||
});
|
||||
if (labels.length !== 0) {
|
||||
if (context.eventName === 'discussion') {
|
||||
// Discussions are not covered by the REST issues API; resolve the label
|
||||
// names to node IDs and attach them with the GraphQL labelable mutation.
|
||||
const labelIds = [];
|
||||
for (const name of labels) {
|
||||
const res = await github.graphql(`
|
||||
query($owner: String!, $repo: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
label(name: $name) { id }
|
||||
}
|
||||
}`, { owner: context.repo.owner, repo: context.repo.repo, name: name });
|
||||
if (res.repository.label) {
|
||||
labelIds.push(res.repository.label.id);
|
||||
}
|
||||
}
|
||||
if (labelIds.length !== 0) {
|
||||
await github.graphql(`
|
||||
mutation($labelableId: ID!, $labelIds: [ID!]!) {
|
||||
addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`, { labelableId: discussionNodeId, labelIds: labelIds });
|
||||
}
|
||||
} else {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueOrPrNumber,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
165
.github/workflows/sponsor-triage.yml
vendored
Normal file
165
.github/workflows/sponsor-triage.yml
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
name: Sponsor Triage
|
||||
|
||||
# Periodically add open issues and PRs opened by sponsors (public and private) to the private "Sponsor Triage" project board
|
||||
# Requires a PAT in secret SPONSOR_TOKEN with scopes:
|
||||
# - project (write project items / fields)
|
||||
# - read:org (search org issues, check Adafruit membership)
|
||||
# - read:user / sponsors (read own sponsorships incl. private)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # every 6 hours
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: sponsor-triage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync sponsor issues to project board
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.SPONSOR_TOKEN }}
|
||||
script: |
|
||||
const PROJECT_OWNER = 'hathach';
|
||||
const PROJECT_NUMBER = 3;
|
||||
// Where to look for sponsor-authored open issues.
|
||||
const SEARCH_SCOPES = ['org:adafruit', 'user:hathach'];
|
||||
|
||||
const tierFromMonthly = (m) => {
|
||||
if (m >= 512) return 'QWORD';
|
||||
if (m >= 128) return 'DWORD';
|
||||
if (m >= 32) return 'Word';
|
||||
if (m >= 8) return 'Byte';
|
||||
if (m >= 2) return 'Bit';
|
||||
return null;
|
||||
};
|
||||
|
||||
// --- 1. Resolve project id + field/option ids (by name, never hardcoded) ---
|
||||
const proj = await github.graphql(`
|
||||
query($owner: String!, $number: Int!) {
|
||||
user(login: $owner) {
|
||||
projectV2(number: $number) {
|
||||
id
|
||||
fields(first: 50) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id name options { id name }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, { owner: PROJECT_OWNER, number: PROJECT_NUMBER });
|
||||
|
||||
const project = proj.user.projectV2;
|
||||
const fieldByName = {};
|
||||
for (const f of project.fields.nodes) {
|
||||
if (f && f.name) {
|
||||
fieldByName[f.name] = { id: f.id, options: {} };
|
||||
for (const o of (f.options || [])) fieldByName[f.name].options[o.name] = o.id;
|
||||
}
|
||||
}
|
||||
const tierField = fieldByName['Tier'];
|
||||
const visField = fieldByName['Visibility'];
|
||||
|
||||
// --- 2. Build sponsor map: loginLower -> { tier, visibility } ---
|
||||
const sponsors = new Map();
|
||||
|
||||
// 2a. GitHub Sponsors of the owner, including private ones.
|
||||
let after = null;
|
||||
for (let page = 0; page < 20; page++) {
|
||||
const res = await github.graphql(`
|
||||
query($owner: String!, $after: String) {
|
||||
user(login: $owner) {
|
||||
sponsorshipsAsMaintainer(includePrivate: true, first: 100, after: $after) {
|
||||
pageInfo { hasNextPage endCursor }
|
||||
nodes {
|
||||
privacyLevel
|
||||
tier { monthlyPriceInDollars }
|
||||
sponsorEntity {
|
||||
... on User { login }
|
||||
... on Organization { login }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, { owner: PROJECT_OWNER, after });
|
||||
const conn = res.user.sponsorshipsAsMaintainer;
|
||||
for (const n of conn.nodes) {
|
||||
const login = n.sponsorEntity && n.sponsorEntity.login;
|
||||
if (!login) continue;
|
||||
const tier = tierFromMonthly((n.tier && n.tier.monthlyPriceInDollars) || 0);
|
||||
const visibility = n.privacyLevel === 'PRIVATE' ? 'Private' : 'Public';
|
||||
sponsors.set(login.toLowerCase(), { login, tier, visibility });
|
||||
}
|
||||
if (!conn.pageInfo.hasNextPage) break;
|
||||
after = conn.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
core.info(`Resolved ${sponsors.size} sponsor account(s).`);
|
||||
if (sponsors.size === 0) return;
|
||||
|
||||
// 2b. Adafruit org members get the Adafruit tier regardless of $ amount.
|
||||
for (const s of sponsors.values()) {
|
||||
try {
|
||||
const m = await github.rest.orgs.checkMembershipForUser({ org: 'adafruit', username: s.login });
|
||||
if (m.status === 204) s.tier = 'Adafruit';
|
||||
} catch (e) { /* not an Adafruit member */ }
|
||||
}
|
||||
|
||||
// --- 3. Find each sponsor's open issues and PRs in the search scopes ---
|
||||
// (search type ISSUE returns both issues and pull requests)
|
||||
const found = new Map(); // contentId -> { tier, visibility }
|
||||
for (const s of sponsors.values()) {
|
||||
for (const scope of SEARCH_SCOPES) {
|
||||
const q = `${scope} is:open author:${s.login}`;
|
||||
try {
|
||||
const res = await github.graphql(`
|
||||
query($q: String!) {
|
||||
search(query: $q, type: ISSUE, first: 100) {
|
||||
nodes { ... on Issue { id } ... on PullRequest { id } }
|
||||
}
|
||||
}`, { q });
|
||||
for (const node of res.search.nodes) {
|
||||
if (node && node.id && !found.has(node.id)) {
|
||||
found.set(node.id, { tier: s.tier, visibility: s.visibility });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Search failed for one scope: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
core.info(`Found ${found.size} open sponsor issue(s) across scopes.`);
|
||||
|
||||
// --- 4. Add to board + set Tier / Visibility (idempotent) ---
|
||||
const setField = async (itemId, field, optionName) => {
|
||||
if (!field || !optionName) return;
|
||||
const optId = field.options[optionName];
|
||||
if (!optId) return;
|
||||
await github.graphql(`
|
||||
mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) {
|
||||
updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $p, itemId: $i, fieldId: $f, value: { singleSelectOptionId: $o }
|
||||
}) { projectV2Item { id } }
|
||||
}`, { p: project.id, i: itemId, f: field.id, o: optId });
|
||||
};
|
||||
|
||||
let added = 0;
|
||||
for (const [contentId, meta] of found) {
|
||||
const res = await github.graphql(`
|
||||
mutation($p: ID!, $c: ID!) {
|
||||
addProjectV2ItemById(input: { projectId: $p, contentId: $c }) {
|
||||
item { id }
|
||||
}
|
||||
}`, { p: project.id, c: contentId });
|
||||
const itemId = res.addProjectV2ItemById.item.id;
|
||||
await setField(itemId, tierField, meta.tier);
|
||||
await setField(itemId, visField, meta.visibility);
|
||||
added++;
|
||||
}
|
||||
core.info(`Synced ${added} item(s) to the board.`);
|
||||
Reference in New Issue
Block a user