From 9dd9d2b71c9f1fb1d4547ce33fc0ff8a1ec1608b Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Wed, 1 Apr 2026 18:22:27 -0400 Subject: [PATCH 01/17] fix(ci): use printf for Linear ticket description newlines Why this change was needed: The Linear ticket body created by the dependabot sync workflow was rendering literal \n characters instead of actual line breaks, making the ticket description unreadable. What changed: - Extracted the ticket description into a printf call that properly interprets \n escape sequences into real newlines - Passed REPO and PR_URL as %s format args instead of inline shell interpolation Problem solved: Linear tickets created by the sync workflow now render with proper Markdown formatting and line breaks. --- .github/workflows/sync_dependabot_python.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 65b5a5d..eb2ffd4 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -210,12 +210,14 @@ jobs: REPO=$(echo "$item" | jq -r '.name') PR_URL=$(echo "$item" | jq -r '.url') + DESC=$(printf "The daily [dependabot config sync](https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml) workflow detected that **%s**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml), and opened a PR to bring it in line.\n\n**PR:** %s\n\n### What to do\n1. Review the PR and confirm the dependabot config looks correct for this repo\n2. Merge when ready\n3. If this repo needs a custom config or should be excluded from syncing, let @naurasj know" "$REPO" "$PR_URL") + curl -s -X POST https://api.linear.app/graphql \ -H "Content-Type: application/json" \ -H "Authorization: $LINEAR_API_KEY" \ -d "$(jq -n \ --arg title "Dependabot config sync — review PR for $REPO" \ - --arg desc "The daily [dependabot config sync](https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml) workflow detected that **${REPO}**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml), and opened a PR to bring it in line.\n\n**PR:** ${PR_URL}\n\n### What to do\n1. Review the PR and confirm the dependabot config looks correct for this repo\n2. Merge when ready\n3. If this repo needs a custom config or should be excluded from syncing, let @naurasj know" \ + --arg desc "$DESC" \ --arg team "$LINEAR_TEAM_ID" \ --arg project "$LINEAR_PROJECT_ID" \ --arg state "$LINEAR_TRIAGE_STATE_ID" \ From 25a0306b4e5f7d43d53821f74427ca0861e97423 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 08:59:02 -0400 Subject: [PATCH 02/17] feat(ci): richer Slack summary with pending PR tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this change was needed: The previous Slack message was a flat, one-size-fits-all summary that always posted — even when there was nothing actionable. Stale sync PRs from previous runs were silently skipped with no visibility. What changed: - Track still-open PRs from previous runs in a `pending` array with their opened date as a gentle nudge for stale reviews - Build a first-person Slack message with distinct sections for newly opened PRs and PRs still awaiting merge - Suppress the Slack post entirely when all repos are in sync and no PRs are outstanding - Move message construction into the script step for cleaner logic Problem solved: The team now gets actionable, context-rich Slack notifications that surface stale PRs, and the channel stays quiet when there's nothing to act on. --- .github/workflows/sync_dependabot_python.yml | 46 +++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index eb2ffd4..283a4c9 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -55,6 +55,7 @@ jobs: }) const opened = [] + const pending = [] const skipped = [] const upToDate = [] const errors = [] @@ -108,7 +109,10 @@ jobs: }) if (existingPRs.length > 0) { - core.info(`${name}: open sync PR already exists — ${existingPRs[0].html_url}`) + const pr = existingPRs[0] + const openedDate = new Date(pr.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + pending.push({ name, url: pr.html_url, openedDate }) + core.info(`${name}: open sync PR already exists — ${pr.html_url}`) continue } @@ -178,16 +182,48 @@ jobs: } } - const summary = { opened, skipped, upToDate, errors } + const summary = { opened, pending, skipped, upToDate, errors } core.setOutput('summary', JSON.stringify(summary)) core.setOutput('opened_count', opened.length) - core.setOutput('slack_text', opened.map(o => `- <${o.url}|${o.name}>`).join('\n')) - core.info(`Done. Opened: ${opened.length}, Up-to-date: ${upToDate.length}, Skipped: ${skipped.length}, Errors: ${errors.length}`) + // ── Build Slack message ──────────────────────────────────────── + const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + const lines = [] + + if (opened.length > 0) { + const s = opened.length === 1 ? '' : 's' + const verb = opened.length === 1 ? 'its' : 'their' + lines.push(`I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) + lines.push('') + lines.push('*Newly opened:*') + for (const o of opened) lines.push(`• <${o.url}|${o.name}>`) + } + + if (pending.length > 0) { + if (opened.length > 0) { + lines.push('') + lines.push('*Still awaiting merge:*') + } else { + lines.push('I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') + lines.push('') + } + for (const p of pending) lines.push(`• <${p.url}|${p.name}> (opened ${p.openedDate})`) + } + + if (lines.length > 0) { + lines.push('') + lines.push(`<${runUrl}|View workflow run>`) + } + + core.setOutput('slack_text', lines.join('\n')) + core.setOutput('should_post_slack', lines.length > 0) + + core.info(`Done. Opened: ${opened.length}, Pending: ${pending.length}, Up-to-date: ${upToDate.length}, Skipped: ${skipped.length}, Errors: ${errors.length}`) - name: Post Slack summary if: >- always() + && steps.sync.outputs.should_post_slack == 'true' && (vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID != '') uses: slackapi/slack-github-action@v2 with: @@ -195,7 +231,7 @@ jobs: token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: ${{ vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID }} - text: "🔄 Dependabot config sync completed.${{ fromJSON(steps.sync.outputs.opened_count) > 0 && format(' PRs opened:\n{0}', steps.sync.outputs.slack_text) || ' All repos are up to date — no PRs opened.' }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>" + text: ${{ toJSON(steps.sync.outputs.slack_text) }} - name: Create Linear tickets if: >- From fa9178e1f7aa00b9f409e8d604c6b8ce3e5b5bbc Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 09:03:57 -0400 Subject: [PATCH 03/17] docs(ci): improve sync PR description to match Linear ticket style Why this change was needed: The PR body opened by the dependabot sync workflow was a terse one-liner that didn't give reviewers enough context about what the PR is or what they should do with it. What changed: - Replaced the minimal PR description with a richer body that explains why the PR was opened and lists clear next steps - Mirrors the tone and structure of the companion Linear ticket Problem solved: Reviewers now see actionable context directly in the PR without needing to cross-reference the Linear ticket. --- .github/workflows/sync_dependabot_python.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 283a4c9..80ecca5 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -169,7 +169,14 @@ jobs: owner: org, repo: name, title: prTitle, - body: 'Synced from the org-level [`dependabot-python-template.yml`](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml).\n\nThis configures Dependabot with CodeArtifact registry credentials and enables weekly updates for pip, Docker, and GitHub Actions ecosystems.', + body: [ + 'The [dependabot config sync workflow](https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml) detected that this repo\'s `.github/dependabot.yml` was either missing or out of date with the [org template](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml), and opened this PR to bring it in line.', + '', + '### What to do', + '1. Review the dependabot config and confirm it looks correct for this repo', + '2. Merge when ready', + '3. If this repo needs a custom config or should be excluded from syncing, let @justanothersynth know' + ].join('\n'), head: branch, base: repo.default_branch }) From a62cbe8bb8a9c6e64f653dc2c777d516d0978e03 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 09:21:54 -0400 Subject: [PATCH 04/17] refactor(ci): create Linear ticket before PR and link them via identifier Why this change was needed: The Linear ticket and PR were created independently with no cross-reference. Reviewers had to manually find the corresponding ticket, and merging the PR didn't update the ticket status. What changed: - Moved Linear ticket creation into the github-script step, called before the PR is created via fetch() to Linear's GraphQL API - PR body now includes "Resolves AMR-" so Linear auto-links the PR and auto-closes the ticket on merge - Linear ticket description drops the explicit PR URL in favor of auto-linking, with a note explaining the link - Both descriptions now mention "for Python repos" for clarity - Removed the separate shell-based "Create Linear tickets" step Problem solved: Linear tickets and PRs are now bidirectionally linked. Merging a sync PR automatically resolves its Linear ticket. --- .github/workflows/sync_dependabot_python.yml | 109 ++++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 80ecca5..df51c03 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -31,6 +31,11 @@ jobs: - name: Sync dependabot config to repos id: sync uses: actions/github-script@v7 + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_AMERA_TEAM_ID }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_DEPENDABOT_ALERTS_PROJECT_ID }} + LINEAR_TRIAGE_STATE_ID: ${{ vars.LINEAR_TRIAGE_STATE_ID }} with: github-token: ${{ steps.app-token.outputs.token }} script: | @@ -39,6 +44,8 @@ jobs: const branch = 'chore/sync-dependabot-config' const targetPath = '.github/dependabot.yml' const prTitle = 'chore: sync dependabot config from org template' + const workflowUrl = 'https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml' + const templateUrl = 'https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml' // ── Skip list ──────────────────────────────────────────────────── // Add repo names here to exclude them from syncing, @@ -48,6 +55,55 @@ jobs: const template = fs.readFileSync('.github/dependabot-python-template.yml', 'utf8') const templateB64 = Buffer.from(template).toString('base64') + /** Creates a Linear issue and returns the identifier (e.g. AMR-42). */ + async function createLinearTicket(repoName) { + const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_TRIAGE_STATE_ID } = process.env + if (!LINEAR_API_KEY || !LINEAR_TEAM_ID) return null + + const description = [ + `The [dependabot config sync workflow](${workflowUrl}) for Python repos detected that **${repoName}**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](${templateUrl}), and opened a PR to bring it in line.`, + '', + '### What to do', + '1. Review the PR and confirm the dependabot config looks correct for this repo', + '2. Merge when ready', + '3. If this repo needs a custom config or should be excluded from syncing, let @naurasj know', + '', + '> The corresponding PR is auto-linked to this ticket.' + ].join('\n') + + const res = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': LINEAR_API_KEY + }, + body: JSON.stringify({ + query: `mutation($input: IssueCreateInput!) { + issueCreate(input: $input) { success issue { identifier url } } + }`, + variables: { + input: { + title: `Dependabot config sync — review PR for ${repoName}`, + description, + teamId: LINEAR_TEAM_ID, + ...(LINEAR_PROJECT_ID && { projectId: LINEAR_PROJECT_ID }), + ...(LINEAR_TRIAGE_STATE_ID && { stateId: LINEAR_TRIAGE_STATE_ID }) + } + } + }) + }) + + const { data } = await res.json() + if (!data?.issueCreate?.success) { + core.warning(`${repoName}: Linear ticket creation failed`) + return null + } + + const ticket = data.issueCreate.issue + core.info(`${repoName}: created Linear ticket ${ticket.identifier} — ${ticket.url}`) + return ticket.identifier + } + const repos = await github.paginate(github.rest.repos.listForOrg, { org, type: 'all', @@ -165,18 +221,23 @@ jobs: ...(fileSha && { sha: fileSha }) }) + const ticketId = await createLinearTicket(name) + + const prBody = [ + `The [dependabot config sync workflow](${workflowUrl}) for Python repos detected that **${name}**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](${templateUrl}), and opened a PR to bring it in line.`, + '', + '### What to do', + '1. Review the dependabot config and confirm it looks correct for this repo', + '2. Merge when ready', + '3. If this repo needs a custom config or should be excluded from syncing, let @justanothersynth know', + ...(ticketId ? ['', `Resolves ${ticketId}`] : []) + ].join('\n') + const { data: pr } = await github.rest.pulls.create({ owner: org, repo: name, title: prTitle, - body: [ - 'The [dependabot config sync workflow](https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml) detected that this repo\'s `.github/dependabot.yml` was either missing or out of date with the [org template](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml), and opened this PR to bring it in line.', - '', - '### What to do', - '1. Review the dependabot config and confirm it looks correct for this repo', - '2. Merge when ready', - '3. If this repo needs a custom config or should be excluded from syncing, let @justanothersynth know' - ].join('\n'), + body: prBody, head: branch, base: repo.default_branch }) @@ -240,35 +301,3 @@ jobs: channel: ${{ vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID }} text: ${{ toJSON(steps.sync.outputs.slack_text) }} - - name: Create Linear tickets - if: >- - always() - && fromJSON(steps.sync.outputs.opened_count) > 0 - && (vars.LINEAR_AMERA_TEAM_ID != '') - run: | - SUMMARY='${{ steps.sync.outputs.summary }}' - - # One ticket per repo, landed in Triage for assignment - echo "$SUMMARY" | jq -c '.opened[]' | while read -r item; do - REPO=$(echo "$item" | jq -r '.name') - PR_URL=$(echo "$item" | jq -r '.url') - - DESC=$(printf "The daily [dependabot config sync](https://github.com/amera-apps/.github/blob/main/.github/workflows/sync_dependabot_python.yml) workflow detected that **%s**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml), and opened a PR to bring it in line.\n\n**PR:** %s\n\n### What to do\n1. Review the PR and confirm the dependabot config looks correct for this repo\n2. Merge when ready\n3. If this repo needs a custom config or should be excluded from syncing, let @naurasj know" "$REPO" "$PR_URL") - - curl -s -X POST https://api.linear.app/graphql \ - -H "Content-Type: application/json" \ - -H "Authorization: $LINEAR_API_KEY" \ - -d "$(jq -n \ - --arg title "Dependabot config sync — review PR for $REPO" \ - --arg desc "$DESC" \ - --arg team "$LINEAR_TEAM_ID" \ - --arg project "$LINEAR_PROJECT_ID" \ - --arg state "$LINEAR_TRIAGE_STATE_ID" \ - '{ query: "mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { url } } }", variables: { input: { title: $title, description: $desc, teamId: $team, projectId: $project, stateId: $state } } }' - )" - done - env: - LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} - LINEAR_TEAM_ID: ${{ vars.LINEAR_AMERA_TEAM_ID }} - LINEAR_PROJECT_ID: ${{ vars.LINEAR_DEPENDABOT_ALERTS_PROJECT_ID }} - LINEAR_TRIAGE_STATE_ID: ${{ vars.LINEAR_TRIAGE_STATE_ID }} From 8ef1d0dc752485ba430bb3a02f5cda78d6247d6e Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 10:56:04 -0400 Subject: [PATCH 05/17] feat(ci): assign, label, and prioritize Linear tickets from sync workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this change was needed: Tickets were landing in Triage with no assignee, label, or priority, requiring manual triage before anyone would act on them. What changed: - Tickets are now assigned to Nauras, labeled "Amerabot", set to Urgent priority, and placed in Todo instead of Triage - Renamed env vars to follow consistent double-underscore naming convention (LINEAR_TEAM_ID__AMERA, LINEAR_PROJECT_ID__SOC2_COMPLIANCE) Problem solved: Sync tickets are immediately actionable — assigned, prioritized, and visible in the right board column without manual triage. --- .github/workflows/sync_dependabot_python.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index df51c03..5ee3da8 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -33,9 +33,11 @@ jobs: uses: actions/github-script@v7 env: LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} - LINEAR_TEAM_ID: ${{ vars.LINEAR_AMERA_TEAM_ID }} - LINEAR_PROJECT_ID: ${{ vars.LINEAR_DEPENDABOT_ALERTS_PROJECT_ID }} - LINEAR_TRIAGE_STATE_ID: ${{ vars.LINEAR_TRIAGE_STATE_ID }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID__AMERA }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID__SOC2_COMPLIANCE }} + LINEAR_STATE_ID: ${{ vars.LINEAR_STATE_ID__TO_DO }} + LINEAR_ASSIGNEE_ID: ${{ vars.LINEAR_PERSON_ID__NAURAS_J }} + LINEAR_LABEL_ID: ${{ vars.LINEAR_LABEL_ID__AMERABOT }} with: github-token: ${{ steps.app-token.outputs.token }} script: | @@ -57,7 +59,7 @@ jobs: /** Creates a Linear issue and returns the identifier (e.g. AMR-42). */ async function createLinearTicket(repoName) { - const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_TRIAGE_STATE_ID } = process.env + const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_STATE_ID, LINEAR_ASSIGNEE_ID, LINEAR_LABEL_ID } = process.env if (!LINEAR_API_KEY || !LINEAR_TEAM_ID) return null const description = [ @@ -86,8 +88,11 @@ jobs: title: `Dependabot config sync — review PR for ${repoName}`, description, teamId: LINEAR_TEAM_ID, + priority: 1, ...(LINEAR_PROJECT_ID && { projectId: LINEAR_PROJECT_ID }), - ...(LINEAR_TRIAGE_STATE_ID && { stateId: LINEAR_TRIAGE_STATE_ID }) + ...(LINEAR_STATE_ID && { stateId: LINEAR_STATE_ID }), + ...(LINEAR_ASSIGNEE_ID && { assigneeId: LINEAR_ASSIGNEE_ID }), + ...(LINEAR_LABEL_ID && { labelIds: [LINEAR_LABEL_ID] }) } } }) From 418b1e28eff9c22b28b1bb3beca16426506b4036 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 10:59:20 -0400 Subject: [PATCH 06/17] chore(ci): bump actions to Node.js 24 runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this change was needed: GitHub Actions is deprecating Node.js 20 runners on June 2, 2026, and the workflow was logging deprecation warnings on every run. What changed: - actions/checkout v4 → v5 - actions/github-script v7 → v8 - slackapi/slack-github-action v2 → v3 Problem solved: Workflow runs cleanly without Node.js 20 deprecation warnings. --- .github/workflows/sync_dependabot_python.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 5ee3da8..b079a01 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate GitHub App token id: app-token @@ -30,7 +30,7 @@ jobs: - name: Sync dependabot config to repos id: sync - uses: actions/github-script@v7 + uses: actions/github-script@v8 env: LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID__AMERA }} @@ -298,7 +298,7 @@ jobs: always() && steps.sync.outputs.should_post_slack == 'true' && (vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID != '') - uses: slackapi/slack-github-action@v2 + uses: slackapi/slack-github-action@v3 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} From 019f2bee8ae8aa01267949c88f95dbf671aee088 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 11:43:05 -0400 Subject: [PATCH 07/17] feat(ci): push updated template to existing sync PRs Why this change was needed: When the org template changed, repos with an already-open sync PR were silently skipped. The stale PR would stay open with outdated content until manually closed and the workflow re-run. What changed: - When an open sync PR exists, compare its branch content against the current template before deciding to skip - If the template has changed, push an updated commit directly to the PR branch so the existing PR reflects the latest config - If the PR branch already matches, skip as before Problem solved: Template updates propagate to all repos automatically, even those with open PRs from a previous run. --- .github/workflows/sync_dependabot_python.yml | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index b079a01..672c333 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -172,8 +172,35 @@ jobs: if (existingPRs.length > 0) { const pr = existingPRs[0] const openedDate = new Date(pr.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + + let prBranchContent = null + let prBranchFileSha = null + try { + const { data } = await github.rest.repos.getContent({ + owner: org, repo: name, path: targetPath, ref: branch + }) + prBranchContent = Buffer.from(data.content, 'base64').toString('utf8') + prBranchFileSha = data.sha + } catch { + // file doesn't exist on PR branch + } + + if (prBranchContent === template) { + pending.push({ name, url: pr.html_url, openedDate }) + core.info(`${name}: open sync PR already up to date`) + continue + } + + await github.rest.repos.createOrUpdateFileContents({ + owner: org, repo: name, path: targetPath, + message: 'chore: update dependabot config to latest org template', + content: templateB64, + branch, + ...(prBranchFileSha && { sha: prBranchFileSha }) + }) + pending.push({ name, url: pr.html_url, openedDate }) - core.info(`${name}: open sync PR already exists — ${pr.html_url}`) + core.info(`${name}: pushed updated template to existing PR — ${pr.html_url}`) continue } From 8ddc7e669739fa0fc1889ae203a2085dee6e10ae Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 11:46:03 -0400 Subject: [PATCH 08/17] docs: update README to reflect workflow changes Why this change was needed: The README referenced old variable names, was missing secrets, and described the old Triage-based ticket flow rather than the current Todo/assigned/labeled setup. What changed: - Added LINEAR_API_KEY and SLACK_BOT_TOKEN to the secrets table - Updated variable names to new double-underscore convention - Added LINEAR_PERSON_ID__NAURAS_J and LINEAR_LABEL_ID__AMERABOT vars - Changed Triage references to Todo - Replaced the numbered "How it works" list with a per-repo decision flowchart showing the template-update-to-existing-PR logic - Added note about automatic propagation to open PRs Problem solved: README now accurately reflects the current workflow behavior and configuration requirements. --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a5480e1..ea6bb9a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ graph TD |---|---| | `AMERABOT_APP_ID` | GitHub App ID | | `AMERABOT_APP_PRIVATE_KEY` | GitHub App private key | +| `LINEAR_API_KEY` | Linear API key (used by `sync_dependabot_python` to create tickets) | +| `SLACK_BOT_TOKEN` | Slack Bot User OAuth Token (used by `sync_dependabot_python` to post summaries) | | `AWS_ACCESS_KEY_ID` | IAM user for CodeArtifact token generation | | `AWS_SECRET_ACCESS_KEY` | IAM user for CodeArtifact token generation | @@ -98,9 +100,11 @@ The AWS IAM user should have minimal permissions: `codeartifact:GetAuthorization | Variable | Description | |---|---| | `SLACK_DEPENDABOT_ALERTS_CHANNEL_ID` | Slack channel (used by `sync_dependabot_python`) | -| `LINEAR_AMERA_TEAM_ID` | Linear team (used by `sync_dependabot_python`) | -| `LINEAR_DEPENDABOT_ALERTS_PROJECT_ID` | Linear project (used by `sync_dependabot_python`) | -| `LINEAR_TRIAGE_STATE_ID` | Linear "Triage" workflow state — tickets land here for immediate visibility | +| `LINEAR_TEAM_ID__AMERA` | Linear team (used by `sync_dependabot_python`) | +| `LINEAR_PROJECT_ID__SOC2_COMPLIANCE` | Linear project (used by `sync_dependabot_python`) | +| `LINEAR_STATE_ID__TO_DO` | Linear "Todo" workflow state — tickets land here assigned and ready to act on | +| `LINEAR_PERSON_ID__NAURAS_J` | Linear user ID — default assignee for sync tickets | +| `LINEAR_LABEL_ID__AMERABOT` | Linear label ID — tags tickets as bot-created | | `AWS_REGION` | AWS region for CodeArtifact (`us-east-1`) | | `AWS_OWNER_ID` | AWS account ID / domain owner (`371568547021`) | @@ -141,17 +145,23 @@ graph TD Check -->|"yes + out of date"| PR["Open PR:\nchore/sync-dependabot-config"] Check -->|"no or up-to-date"| Skip[Skip] PR --> Slack["Slack summary"] - PR --> Linear["Linear ticket per repo\n(Triage)"] + PR --> Linear["Linear ticket per repo\n(Todo, assigned)"] ``` -**How it works:** +**Per-repo decision flow:** -1. Lists all repos in the org -2. For each non-archived repo, checks if `pyproject.toml` exists -3. Compares the repo's `.github/dependabot.yml` to the template — skips if already matching -4. Skips if an open sync PR already exists from a previous run -5. Creates a branch, commits the template, and opens a PR -6. After processing all repos, posts a Slack summary and creates one Linear ticket per repo (in Triage) for each PR opened +```mermaid +flowchart TD + checkTemplate{Default branch matches template?} + checkTemplate -->|Yes| upToDate[Skip — up to date] + checkTemplate -->|No| checkPR{Open sync PR exists?} + checkPR -->|No| createNew[Create branch + commit + Linear ticket + PR] + checkPR -->|Yes| checkPRContent{PR branch matches template?} + checkPRContent -->|Yes| skipPending[Skip — add to pending] + checkPRContent -->|No| pushUpdate[Push updated commit to PR branch, add to pending] +``` + +After processing all repos, posts a Slack summary and creates one Linear ticket per repo for each new PR opened. PRs are opened (not direct pushes) to comply with branch protection rules requiring at least one approving review. @@ -173,3 +183,5 @@ To change the Dependabot config across all repos: 2. Merge to `main` 3. Wait for the next scheduled sync or trigger manually via `workflow_dispatch` 4. Review and merge the PRs opened in each repo + +If repos already have open sync PRs from a previous run, the workflow pushes an updated commit to those PRs automatically — no need to close and re-run. From 23574334a3757265b97c7edf85b7fb5c08777afc Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 11:53:22 -0400 Subject: [PATCH 09/17] docs: updated comments --- .github/dependabot-python-template.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/dependabot-python-template.yml b/.github/dependabot-python-template.yml index 9c8506b..346a967 100644 --- a/.github/dependabot-python-template.yml +++ b/.github/dependabot-python-template.yml @@ -1,6 +1,14 @@ -# Dependabot configuration synced from amera-apps/.github. -# Do not edit in individual repos — changes will be overwritten -# by the sync_dependabot_python workflow. +# Dependabot configuration for Python repos — tells GitHub's Dependabot how +# to check for dependency updates across pip, Docker, and GitHub Actions +# ecosystems. Includes credentials for our private CodeArtifact registry so +# Dependabot can resolve internal packages (e.g. amera-core) when opening +# automated fix and version bump PRs. +# +# This file is synced from the org template at amera-apps/.github. +# ❗ DO NOT edit directly — changes will be overwritten by the next sync run. +# To update, edit the template and let it propagate: +# https://github.com/amera-apps/.github/blob/main/.github/dependabot-python-template.yml +# If you're unsure, reach out to nauras@amerahealthsolutions.com (@justanothersynth) version: 2 From bfff720c71277ced731360ca4ef0bcb480db43a5 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 12:00:09 -0400 Subject: [PATCH 10/17] feat(ci): enrich Slack message with PR numbers and Linear ticket links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this change was needed: The Slack summary only showed repo names as links, requiring readers to click through to find the PR or ticket. No ticket info was shown for pending PRs at all. What changed: - createLinearTicket now returns { identifier, url } instead of just the identifier string - Newly opened items store prNumber, ticketId, and ticketUrl - Pending items parse the ticket ID from the PR body via regex and construct the Linear URL - Slack lines now use the format: *repo* — PR #num · AMR-num with both the PR and ticket hyperlinked - Extracted a shared formatRepoLine helper for consistent formatting Problem solved: Team members can jump directly to the PR or Linear ticket from the Slack message without extra clicks. --- .github/workflows/sync_dependabot_python.yml | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 672c333..ab15518 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -57,7 +57,7 @@ jobs: const template = fs.readFileSync('.github/dependabot-python-template.yml', 'utf8') const templateB64 = Buffer.from(template).toString('base64') - /** Creates a Linear issue and returns the identifier (e.g. AMR-42). */ + /** Creates a Linear issue and returns { identifier, url } or null. */ async function createLinearTicket(repoName) { const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_STATE_ID, LINEAR_ASSIGNEE_ID, LINEAR_LABEL_ID } = process.env if (!LINEAR_API_KEY || !LINEAR_TEAM_ID) return null @@ -106,7 +106,7 @@ jobs: const ticket = data.issueCreate.issue core.info(`${repoName}: created Linear ticket ${ticket.identifier} — ${ticket.url}`) - return ticket.identifier + return { identifier: ticket.identifier, url: ticket.url } } const repos = await github.paginate(github.rest.repos.listForOrg, { @@ -173,6 +173,10 @@ jobs: const pr = existingPRs[0] const openedDate = new Date(pr.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + const ticketMatch = pr.body?.match(/Resolves (AMR-\d+)/) + const ticketId = ticketMatch ? ticketMatch[1] : null + const ticketUrl = ticketId ? `https://linear.app/amera/issue/${ticketId}` : null + let prBranchContent = null let prBranchFileSha = null try { @@ -185,8 +189,10 @@ jobs: // file doesn't exist on PR branch } + const pendingEntry = { name, prUrl: pr.html_url, prNumber: pr.number, ticketId, ticketUrl, openedDate } + if (prBranchContent === template) { - pending.push({ name, url: pr.html_url, openedDate }) + pending.push(pendingEntry) core.info(`${name}: open sync PR already up to date`) continue } @@ -199,7 +205,7 @@ jobs: ...(prBranchFileSha && { sha: prBranchFileSha }) }) - pending.push({ name, url: pr.html_url, openedDate }) + pending.push(pendingEntry) core.info(`${name}: pushed updated template to existing PR — ${pr.html_url}`) continue } @@ -253,7 +259,7 @@ jobs: ...(fileSha && { sha: fileSha }) }) - const ticketId = await createLinearTicket(name) + const ticket = await createLinearTicket(name) const prBody = [ `The [dependabot config sync workflow](${workflowUrl}) for Python repos detected that **${name}**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](${templateUrl}), and opened a PR to bring it in line.`, @@ -262,7 +268,7 @@ jobs: '1. Review the dependabot config and confirm it looks correct for this repo', '2. Merge when ready', '3. If this repo needs a custom config or should be excluded from syncing, let @justanothersynth know', - ...(ticketId ? ['', `Resolves ${ticketId}`] : []) + ...(ticket ? ['', `Resolves ${ticket.identifier}`] : []) ].join('\n') const { data: pr } = await github.rest.pulls.create({ @@ -274,7 +280,13 @@ jobs: base: repo.default_branch }) - opened.push({ name, url: pr.html_url }) + opened.push({ + name, + prUrl: pr.html_url, + prNumber: pr.number, + ticketId: ticket?.identifier ?? null, + ticketUrl: ticket?.url ?? null + }) core.info(`${name}: opened PR — ${pr.html_url}`) } catch (err) { errors.push({ name, error: err.message }) @@ -288,6 +300,14 @@ jobs: // ── Build Slack message ──────────────────────────────────────── const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + + function formatRepoLine({ name, prUrl, prNumber, ticketId, ticketUrl, openedDate }) { + let line = `• *${name}* — <${prUrl}|PR #${prNumber}>` + if (ticketId && ticketUrl) line += ` · <${ticketUrl}|${ticketId}>` + if (openedDate) line += ` (opened ${openedDate})` + return line + } + const lines = [] if (opened.length > 0) { @@ -296,7 +316,7 @@ jobs: lines.push(`I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) lines.push('') lines.push('*Newly opened:*') - for (const o of opened) lines.push(`• <${o.url}|${o.name}>`) + for (const o of opened) lines.push(formatRepoLine(o)) } if (pending.length > 0) { @@ -307,7 +327,7 @@ jobs: lines.push('I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') lines.push('') } - for (const p of pending) lines.push(`• <${p.url}|${p.name}> (opened ${p.openedDate})`) + for (const p of pending) lines.push(formatRepoLine(p)) } if (lines.length > 0) { From 4b76a16e5ac874b3654f208fe9e8eb243828b592 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 12:46:50 -0400 Subject: [PATCH 11/17] feat(ci): LLM-powered auto-assignment for sync tickets and PRs Why this change was needed: All dependabot sync tickets and PRs were assigned to a single person by default, requiring manual reassignment to the actual project lead for each repo. What changed: - Fetch project-mapping.md and person-reference.md from the .cursor repo at workflow start for team/project context - Query Linear for all projects with their leads - Added resolveAssignee() that calls Claude Haiku with structured output (output_config.format / json_schema) to match each repo to its project lead's Linear User ID and GitHub handle - Linear tickets are created with the resolved assignee - GitHub PRs are assigned and review-requested to the resolved user - Falls back to LINEAR_PERSON_ID__NAURAS_J when LLM resolution fails or ANTHROPIC_API_KEY is not configured - Updated README with ANTHROPIC_API_KEY secret and auto-assignment docs Problem solved: Tickets and PRs are automatically routed to the right project lead, eliminating manual triage and assignment. --- .github/workflows/sync_dependabot_python.yml | 158 ++++++++++++++++++- README.md | 7 +- 2 files changed, 159 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index ab15518..b755fa7 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -36,8 +36,9 @@ jobs: LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID__AMERA }} LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID__SOC2_COMPLIANCE }} LINEAR_STATE_ID: ${{ vars.LINEAR_STATE_ID__TO_DO }} - LINEAR_ASSIGNEE_ID: ${{ vars.LINEAR_PERSON_ID__NAURAS_J }} + LINEAR_FALLBACK_ASSIGNEE_ID: ${{ vars.LINEAR_PERSON_ID__NAURAS_J }} LINEAR_LABEL_ID: ${{ vars.LINEAR_LABEL_ID__AMERABOT }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} with: github-token: ${{ steps.app-token.outputs.token }} script: | @@ -57,9 +58,140 @@ jobs: const template = fs.readFileSync('.github/dependabot-python-template.yml', 'utf8') const templateB64 = Buffer.from(template).toString('base64') + // ── Fetch reference data for auto-assignment ─────────────────── + let projectMappingMd = '' + let personReferenceMd = '' + try { + const { data } = await github.rest.repos.getContent({ + owner: org, repo: '.cursor', path: 'skills/amera-index/references/project-mapping.md' + }) + projectMappingMd = Buffer.from(data.content, 'base64').toString('utf8') + } catch { + core.warning('Could not fetch project-mapping.md from .cursor repo') + } + try { + const { data } = await github.rest.repos.getContent({ + owner: org, repo: '.cursor', path: 'skills/amera-index/references/person-reference.md' + }) + personReferenceMd = Buffer.from(data.content, 'base64').toString('utf8') + } catch { + core.warning('Could not fetch person-reference.md from .cursor repo') + } + + let linearProjects = [] + if (process.env.LINEAR_API_KEY) { + try { + const res = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': process.env.LINEAR_API_KEY + }, + body: JSON.stringify({ + query: `{ projects(first: 100) { nodes { name lead { id name displayName email } } } }` + }) + }) + const { data } = await res.json() + linearProjects = (data?.projects?.nodes ?? []) + .filter(p => p.lead) + .map(p => ({ project: p.name, lead: p.lead })) + core.info(`Fetched ${linearProjects.length} Linear projects with leads`) + } catch (err) { + core.warning(`Could not fetch Linear projects: ${err.message}`) + } + } + + /** + * Calls Claude Haiku to resolve the best assignee for a repo. + * Returns { linearUserId, githubHandle } or a fallback. + */ + async function resolveAssignee(repoName) { + const { ANTHROPIC_API_KEY, LINEAR_FALLBACK_ASSIGNEE_ID } = process.env + const fallback = { + linearUserId: LINEAR_FALLBACK_ASSIGNEE_ID || null, + githubHandle: 'justanothersynth' + } + + if (!ANTHROPIC_API_KEY || !projectMappingMd || !personReferenceMd) { + core.info(`${repoName}: skipping LLM assignment — missing API key or reference data`) + return fallback + } + + try { + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: 'claude-haiku-4.5-20250315', + max_tokens: 256, + output_config: { + format: { + type: 'json_schema', + schema: { + type: 'object', + properties: { + linearUserId: { type: ['string', 'null'] }, + githubHandle: { type: ['string', 'null'] } + }, + required: ['linearUserId', 'githubHandle'], + additionalProperties: false + } + } + }, + messages: [{ + role: 'user', + content: [ + `Given the GitHub repo "${repoName}", determine who should be assigned to review a dependabot config sync PR.`, + '', + 'Steps:', + '1. Find the Linear Project for this repo using the GitHub Repo Mapping table', + '2. Find that project\'s lead in the Linear Project Leads data', + '3. Match the lead to a person in the Person Reference Table', + '4. Return their Linear User ID and GitHub Handle', + '', + '## Linear Project Leads', + JSON.stringify(linearProjects, null, 2), + '', + '## GitHub Repo Mapping + Project Keywords', + projectMappingMd, + '', + '## Person Reference Table', + personReferenceMd, + '', + 'Return the Linear User ID and GitHub Handle of the best assignee.', + 'If no confident match can be made, return null for both fields.' + ].join('\n') + }] + }) + }) + + const result = await res.json() + if (result.error) { + core.warning(`${repoName}: Anthropic API error — ${result.error.message}`) + return fallback + } + + const parsed = JSON.parse(result.content[0].text) + if (parsed.linearUserId && parsed.githubHandle) { + core.info(`${repoName}: LLM assigned to ${parsed.githubHandle} (${parsed.linearUserId})`) + return parsed + } + + core.info(`${repoName}: LLM returned null — using fallback assignee`) + return fallback + } catch (err) { + core.warning(`${repoName}: assignee resolution failed — ${err.message}`) + return fallback + } + } + /** Creates a Linear issue and returns { identifier, url } or null. */ - async function createLinearTicket(repoName) { - const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_STATE_ID, LINEAR_ASSIGNEE_ID, LINEAR_LABEL_ID } = process.env + async function createLinearTicket(repoName, assigneeId) { + const { LINEAR_API_KEY, LINEAR_TEAM_ID, LINEAR_PROJECT_ID, LINEAR_STATE_ID, LINEAR_LABEL_ID } = process.env if (!LINEAR_API_KEY || !LINEAR_TEAM_ID) return null const description = [ @@ -91,7 +223,7 @@ jobs: priority: 1, ...(LINEAR_PROJECT_ID && { projectId: LINEAR_PROJECT_ID }), ...(LINEAR_STATE_ID && { stateId: LINEAR_STATE_ID }), - ...(LINEAR_ASSIGNEE_ID && { assigneeId: LINEAR_ASSIGNEE_ID }), + ...(assigneeId && { assigneeId }), ...(LINEAR_LABEL_ID && { labelIds: [LINEAR_LABEL_ID] }) } } @@ -259,7 +391,8 @@ jobs: ...(fileSha && { sha: fileSha }) }) - const ticket = await createLinearTicket(name) + const assignee = await resolveAssignee(name) + const ticket = await createLinearTicket(name, assignee.linearUserId) const prBody = [ `The [dependabot config sync workflow](${workflowUrl}) for Python repos detected that **${name}**'s \`.github/dependabot.yml\` was either missing or out of date with the [org template](${templateUrl}), and opened a PR to bring it in line.`, @@ -280,6 +413,21 @@ jobs: base: repo.default_branch }) + if (assignee.githubHandle) { + try { + await github.rest.issues.addAssignees({ + owner: org, repo: name, issue_number: pr.number, + assignees: [assignee.githubHandle] + }) + await github.rest.pulls.requestReviewers({ + owner: org, repo: name, pull_number: pr.number, + reviewers: [assignee.githubHandle] + }) + } catch (err) { + core.warning(`${name}: could not assign ${assignee.githubHandle} — ${err.message}`) + } + } + opened.push({ name, prUrl: pr.html_url, diff --git a/README.md b/README.md index ea6bb9a..130b1db 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ graph TD | `AMERABOT_APP_PRIVATE_KEY` | GitHub App private key | | `LINEAR_API_KEY` | Linear API key (used by `sync_dependabot_python` to create tickets) | | `SLACK_BOT_TOKEN` | Slack Bot User OAuth Token (used by `sync_dependabot_python` to post summaries) | +| `ANTHROPIC_API_KEY` | Anthropic API key (used by `sync_dependabot_python` for LLM-based assignee resolution) | | `AWS_ACCESS_KEY_ID` | IAM user for CodeArtifact token generation | | `AWS_SECRET_ACCESS_KEY` | IAM user for CodeArtifact token generation | @@ -103,7 +104,7 @@ The AWS IAM user should have minimal permissions: `codeartifact:GetAuthorization | `LINEAR_TEAM_ID__AMERA` | Linear team (used by `sync_dependabot_python`) | | `LINEAR_PROJECT_ID__SOC2_COMPLIANCE` | Linear project (used by `sync_dependabot_python`) | | `LINEAR_STATE_ID__TO_DO` | Linear "Todo" workflow state — tickets land here assigned and ready to act on | -| `LINEAR_PERSON_ID__NAURAS_J` | Linear user ID — default assignee for sync tickets | +| `LINEAR_PERSON_ID__NAURAS_J` | Linear user ID — fallback assignee when LLM resolution fails | | `LINEAR_LABEL_ID__AMERABOT` | Linear label ID — tags tickets as bot-created | | `AWS_REGION` | AWS region for CodeArtifact (`us-east-1`) | | `AWS_OWNER_ID` | AWS account ID / domain owner (`371568547021`) | @@ -165,6 +166,10 @@ After processing all repos, posts a Slack summary and creates one Linear ticket PRs are opened (not direct pushes) to comply with branch protection rules requiring at least one approving review. +#### Auto-assignment + +Each new PR and Linear ticket is automatically assigned to the project lead inferred via an LLM (Claude Haiku). The workflow fetches [`project-mapping.md`](https://github.com/amera-apps/.cursor/blob/main/skills/amera-index/references/project-mapping.md) and [`person-reference.md`](https://github.com/amera-apps/.cursor/blob/main/skills/amera-index/references/person-reference.md) from the `.cursor` repo, queries Linear for project leads, and passes all three data sources to Claude to resolve the best assignee for each repo. If no confident match is found or the `ANTHROPIC_API_KEY` secret is not set, assignment falls back to `LINEAR_PERSON_ID__NAURAS_J`. + #### Skipping repos Some repos may need a custom `dependabot.yml` or should be excluded entirely. Add them to the `skipRepos` array at the top of the `actions/github-script` block in `sync_dependabot_python.yml`: From 15a0cd0ba9f321bf505c9c89ffbfbe8209c74494 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Thu, 2 Apr 2026 12:53:50 -0400 Subject: [PATCH 12/17] chore: updated slack message formatting --- .github/workflows/sync_dependabot_python.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index b755fa7..13b1b91 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -451,7 +451,7 @@ jobs: function formatRepoLine({ name, prUrl, prNumber, ticketId, ticketUrl, openedDate }) { let line = `• *${name}* — <${prUrl}|PR #${prNumber}>` - if (ticketId && ticketUrl) line += ` · <${ticketUrl}|${ticketId}>` + if (ticketId && ticketUrl) line += ` — <${ticketUrl}|${ticketId}>` if (openedDate) line += ` (opened ${openedDate})` return line } @@ -500,4 +500,3 @@ jobs: payload: | channel: ${{ vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID }} text: ${{ toJSON(steps.sync.outputs.slack_text) }} - From d4b0e371e5d8b984640acdba87a6ec0f2f4a2bb3 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Fri, 3 Apr 2026 09:34:22 -0400 Subject: [PATCH 13/17] fix: use the correct haiku model --- .github/workflows/sync_dependabot_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 13b1b91..4d668da 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -126,7 +126,7 @@ jobs: 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ - model: 'claude-haiku-4.5-20250315', + model: 'claude-haiku-4-5-20251001', max_tokens: 256, output_config: { format: { From 364e9ec79ff5327b8ca45c2cd81e6f67f73c80e8 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Fri, 3 Apr 2026 10:07:24 -0400 Subject: [PATCH 14/17] feat(ci): add assignee full names to Slack summary and retry failed PR assignments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why this change was needed: The Slack summary posted by the dependabot config sync workflow only showed repo, PR number, and Linear ticket — reviewers had to click through to see who owned each PR. Additionally, PRs from earlier runs that failed GitHub assignment (e.g. due to missing app permissions) were left permanently unassigned. What changed: - Extended resolveAssignee to accept an optional GitHub handle hint and return fullName alongside linearUserId and githubHandle - Added fullName to the LLM JSON schema and prompt instructions - For pending PRs: read the existing assignee from the PR, pass as a hint to resolveAssignee, and retry assignment if the PR was unassigned - Carry assigneeName through opened/pending entries to formatRepoLine - Updated formatRepoLine to display the assignee's full name in Slack Problem solved: Team members can now see at a glance who owns each sync PR directly in Slack, and previously-unassigned PRs self-heal on subsequent runs. --- .github/workflows/sync_dependabot_python.yml | 60 +++++++++++++++----- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 4d668da..3dcb35e 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -103,13 +103,16 @@ jobs: /** * Calls Claude Haiku to resolve the best assignee for a repo. - * Returns { linearUserId, githubHandle } or a fallback. + * When knownGithubHandle is provided (e.g. from an existing PR), + * it is passed as a hint for a more confident match. + * Returns { linearUserId, githubHandle, fullName } or a fallback. */ - async function resolveAssignee(repoName) { + async function resolveAssignee(repoName, knownGithubHandle = null) { const { ANTHROPIC_API_KEY, LINEAR_FALLBACK_ASSIGNEE_ID } = process.env const fallback = { linearUserId: LINEAR_FALLBACK_ASSIGNEE_ID || null, - githubHandle: 'justanothersynth' + githubHandle: 'justanothersynth', + fullName: 'Nauras Jabari' } if (!ANTHROPIC_API_KEY || !projectMappingMd || !personReferenceMd) { @@ -118,6 +121,10 @@ jobs: } try { + const hintBlock = knownGithubHandle + ? [`\nThe PR is currently assigned to GitHub handle "${knownGithubHandle}". Look this person up in the Person Reference Table and return their details.\n`] + : [] + const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { @@ -135,9 +142,10 @@ jobs: type: 'object', properties: { linearUserId: { type: ['string', 'null'] }, - githubHandle: { type: ['string', 'null'] } + githubHandle: { type: ['string', 'null'] }, + fullName: { type: ['string', 'null'] } }, - required: ['linearUserId', 'githubHandle'], + required: ['linearUserId', 'githubHandle', 'fullName'], additionalProperties: false } } @@ -147,11 +155,12 @@ jobs: content: [ `Given the GitHub repo "${repoName}", determine who should be assigned to review a dependabot config sync PR.`, '', + ...hintBlock, 'Steps:', '1. Find the Linear Project for this repo using the GitHub Repo Mapping table', '2. Find that project\'s lead in the Linear Project Leads data', '3. Match the lead to a person in the Person Reference Table', - '4. Return their Linear User ID and GitHub Handle', + '4. Return their Linear User ID, GitHub Handle, and full Name', '', '## Linear Project Leads', JSON.stringify(linearProjects, null, 2), @@ -162,8 +171,8 @@ jobs: '## Person Reference Table', personReferenceMd, '', - 'Return the Linear User ID and GitHub Handle of the best assignee.', - 'If no confident match can be made, return null for both fields.' + 'Return the Linear User ID, GitHub Handle, and full Name of the best assignee.', + 'If no confident match can be made, return null for all fields.' ].join('\n') }] }) @@ -177,7 +186,7 @@ jobs: const parsed = JSON.parse(result.content[0].text) if (parsed.linearUserId && parsed.githubHandle) { - core.info(`${repoName}: LLM assigned to ${parsed.githubHandle} (${parsed.linearUserId})`) + core.info(`${repoName}: LLM assigned to ${parsed.fullName} / ${parsed.githubHandle} (${parsed.linearUserId})`) return parsed } @@ -309,6 +318,31 @@ jobs: const ticketId = ticketMatch ? ticketMatch[1] : null const ticketUrl = ticketId ? `https://linear.app/amera/issue/${ticketId}` : null + const prAssigneeHandle = pr.assignees?.[0]?.login ?? null + const assignee = await resolveAssignee(name, prAssigneeHandle) + + if (!prAssigneeHandle && assignee.githubHandle) { + try { + await github.rest.issues.addAssignees({ + owner: org, repo: name, issue_number: pr.number, + assignees: [assignee.githubHandle] + }) + await github.rest.pulls.requestReviewers({ + owner: org, repo: name, pull_number: pr.number, + reviewers: [assignee.githubHandle] + }) + core.info(`${name}: retried assignment — assigned to ${assignee.githubHandle}`) + } catch (err) { + core.warning(`${name}: retry assignment failed — ${err.message}`) + } + } + + const pendingEntry = { + name, prUrl: pr.html_url, prNumber: pr.number, + ticketId, ticketUrl, openedDate, + assigneeName: assignee.fullName ?? null + } + let prBranchContent = null let prBranchFileSha = null try { @@ -321,8 +355,6 @@ jobs: // file doesn't exist on PR branch } - const pendingEntry = { name, prUrl: pr.html_url, prNumber: pr.number, ticketId, ticketUrl, openedDate } - if (prBranchContent === template) { pending.push(pendingEntry) core.info(`${name}: open sync PR already up to date`) @@ -433,7 +465,8 @@ jobs: prUrl: pr.html_url, prNumber: pr.number, ticketId: ticket?.identifier ?? null, - ticketUrl: ticket?.url ?? null + ticketUrl: ticket?.url ?? null, + assigneeName: assignee.fullName ?? null }) core.info(`${name}: opened PR — ${pr.html_url}`) } catch (err) { @@ -449,9 +482,10 @@ jobs: // ── Build Slack message ──────────────────────────────────────── const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` - function formatRepoLine({ name, prUrl, prNumber, ticketId, ticketUrl, openedDate }) { + function formatRepoLine({ name, prUrl, prNumber, ticketId, ticketUrl, openedDate, assigneeName }) { let line = `• *${name}* — <${prUrl}|PR #${prNumber}>` if (ticketId && ticketUrl) line += ` — <${ticketUrl}|${ticketId}>` + if (assigneeName) line += ` — ${assigneeName}` if (openedDate) line += ` (opened ${openedDate})` return line } From 3bb836a8effdcc29671ca8501978d2ce4f93edd8 Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Fri, 3 Apr 2026 10:13:06 -0400 Subject: [PATCH 15/17] chore(ci): disable Slack link and media unfurling for sync summary The Linear integration was auto-unfurling every ticket link into a preview card, producing a wall of 11 attachments beneath the summary. Disabling unfurl_links and unfurl_media keeps the message compact. --- .github/workflows/sync_dependabot_python.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 3dcb35e..43249ae 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -534,3 +534,5 @@ jobs: payload: | channel: ${{ vars.SLACK_DEPENDABOT_ALERTS_CHANNEL_ID }} text: ${{ toJSON(steps.sync.outputs.slack_text) }} + unfurl_links: false + unfurl_media: false From b3e37328645d08eadc7433fd9828f46c26adf69f Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Fri, 3 Apr 2026 12:06:45 -0400 Subject: [PATCH 16/17] chore(ci): prefix Slack summary with dependabot.yml sync service label Makes it immediately clear which bot service posted the message when scanning a busy Slack channel. --- .github/workflows/sync_dependabot_python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index 43249ae..cc5dd13 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -495,7 +495,7 @@ jobs: if (opened.length > 0) { const s = opened.length === 1 ? '' : 's' const verb = opened.length === 1 ? 'its' : 'their' - lines.push(`I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) + lines.push(`*\`dependabot.yml\`* sync service — I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) lines.push('') lines.push('*Newly opened:*') for (const o of opened) lines.push(formatRepoLine(o)) @@ -506,7 +506,7 @@ jobs: lines.push('') lines.push('*Still awaiting merge:*') } else { - lines.push('I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') + lines.push('*`dependabot.yml`* sync service — I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') lines.push('') } for (const p of pending) lines.push(formatRepoLine(p)) From f6e2240d183bc3784f6948ef143c8f53689d4bec Mon Sep 17 00:00:00 2001 From: Nauras Jabari Date: Fri, 3 Apr 2026 12:08:27 -0400 Subject: [PATCH 17/17] chore(ci): add emoji and tweak Slack service label formatting --- .github/workflows/sync_dependabot_python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync_dependabot_python.yml b/.github/workflows/sync_dependabot_python.yml index cc5dd13..3c35074 100644 --- a/.github/workflows/sync_dependabot_python.yml +++ b/.github/workflows/sync_dependabot_python.yml @@ -495,7 +495,7 @@ jobs: if (opened.length > 0) { const s = opened.length === 1 ? '' : 's' const verb = opened.length === 1 ? 'its' : 'their' - lines.push(`*\`dependabot.yml\`* sync service — I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) + lines.push(`🔄 *\`dependabot.yml\` sync service* — I just reviewed all our Python repos and opened ${opened.length} new PR${s} to sync ${verb} dependabot config with the org template.`) lines.push('') lines.push('*Newly opened:*') for (const o of opened) lines.push(formatRepoLine(o)) @@ -506,7 +506,7 @@ jobs: lines.push('') lines.push('*Still awaiting merge:*') } else { - lines.push('*`dependabot.yml`* sync service — I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') + lines.push('🔄 *`dependabot.yml` sync service* — I just reviewed all our Python repos — everything is in sync, but some PRs from previous runs are still awaiting merge:') lines.push('') } for (const p of pending) lines.push(formatRepoLine(p))