feat(lark): sidecar credential broker for sandbox lark-cli (Pattern B) #5261
Workflow file for this run
This file contains hidden or 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
| name: Triage | |
| # One workflow for all event-driven PR/issue labeling. Replaces the former | |
| # pr-labeler / pr-triage / issue-triage workflows (and drops actions/labeler). | |
| # | |
| # Design notes: | |
| # * All jobs are pure-metadata: they read changed-file lists / PR fields / the | |
| # review payload via the API and write labels. PR code is NEVER checked out | |
| # or executed, so pull_request_target is safe here. | |
| # * Each job only reconciles labels in namespaces IT owns | |
| # (area:* / size/* / risk:* / needs-validation). It never touches labels | |
| # applied by maintainers or other tools (bug, priority, etc.). first-time- | |
| # contributor and reviewing are add-only. | |
| # * State is read LIVE (listFiles + listLabelsOnIssue) at run time, not from | |
| # the (stale) event payload, so rapid synchronize events converge instead | |
| # of thrashing. | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| pull_request_review: | |
| types: [submitted] | |
| issues: | |
| types: [opened] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| # ── PR: area / size / risk / needs-validation / first-time ───────────────── | |
| pr-labels: | |
| if: github.event_name == 'pull_request_target' && github.event.pull_request.draft == false | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: triage-pr-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Apply PR labels from live state | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const { owner, repo } = context.repo; | |
| const num = pr.number; | |
| // ---- live changed files ---- | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, repo, pull_number: num, per_page: 100, | |
| }); | |
| const paths = files.map(f => f.filename); | |
| const m = (re) => paths.some(p => re.test(p)); | |
| // ---- area: replaces .github/labeler.yml (path -> area) ---- | |
| const AREA_RULES = [ | |
| ['area:frontend', [/^frontend\//]], | |
| ['area:backend', [/^backend\/app\//, /^backend\/packages\/harness\/deerflow\/(runtime|persistence|config|tools|guardrails|tracing|models|utils|uploads)\//]], | |
| ['area:agents', [/^backend\/packages\/harness\/deerflow\/(agents|subagents|reflection)\//, /(^|\/)langgraph\.json$/, /^backend\/.*\/prompts\//]], | |
| ['area:sandbox', [/^docker\//, /^backend\/packages\/harness\/deerflow\/sandbox\//, /(^|\/)Dockerfile$/]], | |
| ['area:skills', [/^skills\//, /^backend\/packages\/harness\/deerflow\/skills\//, /^frontend\/src\/core\/skills\//]], | |
| ['area:mcp', [/^backend\/packages\/harness\/deerflow\/mcp\//, /^frontend\/src\/core\/mcp\//]], | |
| ['area:ci', [/^\.github\//, /^scripts\//]], | |
| ['area:docs', [/^docs\//, /\.mdx?$/]], | |
| ['area:deps', [/(^|\/)(pyproject\.toml|uv\.lock|package\.json|pnpm-lock\.yaml)$/]], | |
| ]; | |
| const areaLabels = AREA_RULES | |
| .filter(([, res]) => res.some(re => m(re))) | |
| .map(([label]) => label); | |
| // ---- size: additions+deletions, excluding lockfiles/snapshots ---- | |
| const EXCLUDE_SIZE = /(^|\/)(uv\.lock|pnpm-lock\.yaml|package-lock\.json)$|\.snap$/; | |
| const churn = files | |
| .filter(f => !EXCLUDE_SIZE.test(f.filename)) | |
| .reduce((s, f) => s + (f.additions || 0) + (f.deletions || 0), 0); | |
| const sizeLabel = | |
| churn < 20 ? 'size/XS' : | |
| churn < 100 ? 'size/S' : | |
| churn < 300 ? 'size/M' : | |
| churn < 700 ? 'size/L' : 'size/XL'; | |
| // ---- risk ---- | |
| const docsOnly = paths.length > 0 && paths.every(p => | |
| /\.(md|mdx|txt)$/i.test(p) || p.startsWith('docs/') || | |
| /\.(png|jpe?g|gif|svg|webp|ico)$/i.test(p)); | |
| const highRisk = | |
| m(/^backend\/app\/gateway\//) || | |
| m(/^backend\/packages\/harness\/deerflow\/(agents|subagents|sandbox)\//) || | |
| m(/(^|\/)langgraph\.json$/) || | |
| m(/(^|\/)(auth|authz|security)/i) || | |
| m(/(pyproject\.toml|uv\.lock|package\.json|pnpm-lock\.yaml)$/) || | |
| m(/^docker\//) || | |
| m(/^\.github\/workflows\//); | |
| const riskLabel = docsOnly ? 'risk:low' : (highRisk ? 'risk:high' : 'risk:medium'); | |
| // ---- needs-validation: front/back contract surface ---- | |
| const contract = | |
| m(/^backend\/app\/gateway\//) || | |
| m(/^backend\/packages\/harness\/deerflow\/(agents|subagents)\//) || | |
| m(/(^|\/)langgraph\.json$/) || | |
| m(/^frontend\/src\/core\/(api|threads|messages)\//); | |
| // ---- live current labels (NOT the stale event payload) ---- | |
| const current = (await github.paginate(github.rest.issues.listLabelsOnIssue, { | |
| owner, repo, issue_number: num, per_page: 100, | |
| })).map(l => l.name); | |
| const hasSkip = current.includes('skip-validation'); | |
| // Reconcile ONLY namespaces we own; never touch others. | |
| const owned = (n) => | |
| n.startsWith('area:') || n.startsWith('size/') || | |
| n.startsWith('risk:') || n === 'needs-validation'; | |
| const desired = new Set([...areaLabels, sizeLabel, riskLabel]); | |
| if (contract && !hasSkip) desired.add('needs-validation'); | |
| const toRemove = current.filter(n => owned(n) && !desired.has(n)); | |
| const toAdd = [...desired].filter(n => !current.includes(n)); | |
| // first-time-contributor: add-only, on opened, real users only. | |
| if (context.payload.action === 'opened' && | |
| pr.user.type === 'User' && | |
| ['FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER'].includes(pr.author_association) && | |
| !current.includes('first-time-contributor')) { | |
| toAdd.push('first-time-contributor'); | |
| } | |
| for (const name of toRemove) { | |
| try { | |
| await github.rest.issues.removeLabel({ owner, repo, issue_number: num, name }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| if (toAdd.length) { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number: num, labels: toAdd }); | |
| } | |
| core.info(`area=[${areaLabels.join(',')}] ${sizeLabel} ${riskLabel} churn=${churn} ` + | |
| `validation=${desired.has('needs-validation')} ` + | |
| `(+${toAdd.join(',') || '-'} / -${toRemove.join(',') || '-'})`); | |
| # ── PR: reviewing label on a maintainer's human review ───────────────────── | |
| reviewing: | |
| if: github.event_name == 'pull_request_review' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: triage-review-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| steps: | |
| - name: Add reviewing label for maintainer reviews | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const num = context.payload.pull_request.number; | |
| const review = context.payload.review; | |
| const assoc = review.author_association; // payload field; no API call | |
| const type = review.user && review.user.type; | |
| // author_association is NONE for every automated reviewer | |
| // (Copilot, CodeRabbit, Codex, Sourcery, ...), so this allowlist | |
| // drops them all without a denylist — and never calls the | |
| // collaborators API that 404s on "Copilot is not a user". | |
| // user.type === 'User' guards the rare bot-added-as-collaborator case. | |
| if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc) || type !== 'User') { | |
| core.info(`reviewer ${review.user && review.user.login} assoc=${assoc} type=${type}; skipping.`); | |
| return; | |
| } | |
| const labels = (await github.paginate(github.rest.issues.listLabelsOnIssue, { | |
| owner, repo, issue_number: num, per_page: 100, | |
| })).map(l => l.name); | |
| if (labels.includes('reviewing')) { | |
| core.info('Already labeled reviewing; skipping.'); | |
| return; | |
| } | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: num, labels: ['reviewing'], | |
| }); | |
| core.info('Added "reviewing".'); | |
| } catch (e) { | |
| if (e.status === 403) core.info('No permission to label (expected on some fork PRs).'); | |
| else throw e; | |
| } | |
| # ── Issue: needs-triage on every new issue ──────────────────────────────── | |
| issue-triage: | |
| if: github.event_name == 'issues' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: triage-issue-${{ github.event.issue.number }} | |
| cancel-in-progress: false | |
| steps: | |
| - name: Add needs-triage label | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.issue.number; | |
| // Read live labels (not the event payload) so labels added at creation | |
| // time via the API or by another automation are seen — consistent with | |
| // the live-state reads in the PR jobs above. | |
| const current = (await github.paginate(github.rest.issues.listLabelsOnIssue, { | |
| owner, repo, issue_number, per_page: 100, | |
| })).map(l => l.name); | |
| if (current.includes('needs-triage')) { | |
| core.info('Issue already has needs-triage; nothing to do.'); | |
| return; | |
| } | |
| // Self-heal: create the label if it does not exist yet. | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner, repo, name: 'needs-triage', color: 'fef2c0', | |
| description: 'Awaiting maintainer triage', | |
| }); | |
| } catch (e) { | |
| if (e.status !== 422) throw e; // 422 = already exists | |
| } | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number, labels: ['needs-triage'], | |
| }); | |
| core.info(`Added needs-triage to #${issue_number}.`); |