summaryrefslogtreecommitdiff
path: root/.github/workflows
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-06-04 15:31:45 +0700
committerhathach <[email protected]>2026-06-04 15:31:45 +0700
commit0393a5eaa1fc9ef33cbfd847c924cb83d82696f3 (patch)
treeb96374c05142757820304e2d89d6f260b24f1354 /.github/workflows
parent232315d6909d9ca7948c830a5e43ad35189da57f (diff)
parentac32feafeb2fb0f6cc5c3010d32db319ff15ca64 (diff)
Merge branch 'master' into dwc2_postfix
Diffstat (limited to '.github/workflows')
-rw-r--r--.github/workflows/labeler.yml159
-rw-r--r--.github/workflows/sponsor-triage.yml165
2 files changed, 299 insertions, 25 deletions
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index c3cc59d0d..fe09413f1 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -4,23 +4,29 @@ on:
issues:
types: [opened]
pull_request_target:
- types: [opened]
+ types: [opened, synchronize, reopened]
+ discussion:
+ types: [created]
jobs:
label-priority:
+ # Author-based priority labels: only on issue/PR/discussion creation, not on PR updates.
+ if: github.event_name != 'pull_request_target' || github.event.action == 'opened'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
+ discussions: write
steps:
- - name: Label New Issue or PR
- uses: actions/github-script@v7
+ - name: Label New Issue, PR or Discussion
+ uses: actions/github-script@v8
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 +34,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 +122,61 @@ 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
+ });
+ }
}
+
+ # Path-based Port labels: attach "Port <ip>" when a PR touches the matching
+ # dcd/hcd driver under src/portable/. Mapping lives in .github/labeler.yml.
+ label-port:
+ if: github.event_name == 'pull_request_target'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ issues: write # allow auto-creating a Port label that doesn't exist yet
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ configuration-path: .github/labeler.yml
+ # sync-labels so a Port label is removed once a PR no longer touches
+ # that driver (job reruns on synchronize). Only labels listed in
+ # labeler.yml are managed, so author-based Prio/Sponsor labels are untouched.
+ sync-labels: true
diff --git a/.github/workflows/sponsor-triage.yml b/.github/workflows/sponsor-triage.yml
new file mode 100644
index 000000000..48b2ac7cc
--- /dev/null
+++ b/.github/workflows/sponsor-triage.yml
@@ -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@v8
+ 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.`);