test: Phase 4B GitHub API Integration Tests (118 tests) #13996
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: Projects • Add & Sync meta from labels | |
| on: | |
| issues: | |
| types: [opened, edited, labeled, unlabeled, reopened, closed] | |
| pull_request: | |
| branches: [develop] | |
| types: | |
| [opened, edited, labeled, unlabeled, reopened, ready_for_review, closed] | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| env: | |
| # Prefer explicit URL; derive from LS_PROJECT_NUMBER if only number is set. | |
| # e.g. set vars.LS_PROJECT_NUMBER=33 → https://github.com/orgs/lightspeedwp/projects/33 | |
| PROJECT_URL: ${{ vars.LS_PROJECT_URL || (vars.LS_PROJECT_NUMBER && format('https://github.com/orgs/{0}/projects/{1}', github.repository_owner, vars.LS_PROJECT_NUMBER)) || '' }} | |
| SLA_WARN_DAYS: "7" | |
| SLA_BREACH_DAYS: "14" | |
| concurrency: | |
| group: project-meta-sync-${{ github.event_name }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| add-and-sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Preflight project sync configuration | |
| id: preflight | |
| env: | |
| APP_ID: ${{ vars.LS_APP_ID }} | |
| APP_PRIVATE_KEY: ${{ secrets.LS_APP_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| enabled="true" | |
| reason="" | |
| # If neither PROJECT_URL nor LS_PROJECT_NUMBER is set, disable gracefully | |
| if [ -z "${PROJECT_URL:-}" ]; then | |
| enabled="false" | |
| reason="Neither LS_PROJECT_URL nor LS_PROJECT_NUMBER is configured (project sync disabled)" | |
| echo "::notice::Project sync disabled: $reason" | |
| # If PROJECT_URL is set but app credentials are missing, fail loudly | |
| elif [ -z "${APP_ID:-}" ] || [ -z "${APP_PRIVATE_KEY:-}" ]; then | |
| echo "::error::Project sync REQUIRES GitHub App credentials (LS_PROJECT_URL is set)" | |
| echo "Missing:" | |
| [ -z "${APP_ID:-}" ] && echo " - vars.LS_APP_ID" | |
| [ -z "${APP_PRIVATE_KEY:-}" ] && echo " - secrets.LS_APP_PRIVATE_KEY" | |
| echo "" | |
| echo "Setup: 1. Create GitHub App in org settings (AI Ops + Project v2 scopes)" | |
| echo " 2. Set vars.LS_APP_ID and secrets.LS_APP_PRIVATE_KEY" | |
| echo " 3. Re-run this workflow" | |
| echo "" | |
| echo "Docs: see .github/workflows/project-meta-sync.yml and docs/AUTOMATION.md" | |
| exit 1 | |
| fi | |
| echo "enabled=$enabled" >> "$GITHUB_OUTPUT" | |
| if [ "$enabled" = "true" ]; then | |
| echo "✅ Project sync enabled (credentials present)" | |
| fi | |
| - name: Create GitHub App installation token | |
| id: app-token | |
| if: steps.preflight.outputs.enabled == 'true' | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ vars.LS_APP_ID }} | |
| private-key: ${{ secrets.LS_APP_PRIVATE_KEY }} | |
| # Without explicit permission-* inputs the token inherits every | |
| # permission the App installation holds. Scope it to what this job | |
| # actually does: | |
| # organization-projects : add-to-project and update-project-fields | |
| # operate on the org Projects v2 board | |
| # issues / pull-requests: resolve the item being added to the board | |
| permission-organization-projects: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: Add item to project (new issues/PRs) | |
| id: addp | |
| if: steps.preflight.outputs.enabled == 'true' | |
| uses: actions/add-to-project@v1.0.2 | |
| with: | |
| project-url: ${{ env.PROJECT_URL }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| - name: Checkout repository | |
| if: steps.preflight.outputs.enabled == 'true' | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 1 | |
| - name: Set up Node.js | |
| if: steps.preflight.outputs.enabled == 'true' | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version-file: ".nvmrc" | |
| - name: Install workflow dependencies | |
| if: steps.preflight.outputs.enabled == 'true' | |
| run: npm ci --ignore-scripts | |
| - name: Collect labels from item | |
| id: collect-labels | |
| if: steps.preflight.outputs.enabled == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const labels = context.payload.issue?.labels || context.payload.pull_request?.labels || []; | |
| const labelNames = labels.map((label) => label.name).filter(Boolean).join('\n'); | |
| core.setOutput('labels', labelNames); | |
| - name: Derive Status/Priority/Type from canonical issue fields | |
| id: derive | |
| if: steps.preflight.outputs.enabled == 'true' | |
| run: node scripts/agents/includes/derive-project-fields.cjs | |
| env: | |
| LABELS: ${{ steps.collect-labels.outputs.labels }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| PR_MERGED: ${{ github.event.pull_request.merged }} | |
| ITEM_CREATED_AT: ${{ github.event.issue.created_at || github.event.pull_request.created_at }} | |
| ITEM_MILESTONE_DUE_ON: ${{ github.event.issue.milestone.due_on || github.event.pull_request.milestone.due_on }} | |
| ITEM_TITLE: ${{ github.event.issue.title || github.event.pull_request.title }} | |
| ITEM_BODY: ${{ github.event.issue.body || github.event.pull_request.body }} | |
| PR_HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} | |
| ISSUE_FIELDS_CONFIG: .github/issue-fields.yml | |
| - name: Update project fields | |
| if: steps.preflight.outputs.enabled == 'true' && steps.addp.outputs.itemId != '' | |
| uses: titoportas/update-project-fields@v0.1.0 | |
| with: | |
| project-url: ${{ env.PROJECT_URL }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| item-id: ${{ steps.addp.outputs.itemId }} | |
| field-keys: Status,Priority,Type,Effort | |
| field-values: ${{ steps.derive.outputs.status }},${{ steps.derive.outputs.priority }},${{ steps.derive.outputs.type }},${{ steps.derive.outputs.effort }} | |
| - name: Update kickoff dates | |
| if: steps.preflight.outputs.enabled == 'true' && steps.addp.outputs.itemId != '' && (steps.derive.outputs.start_date != '' || steps.derive.outputs.target_date != '') | |
| uses: titoportas/update-project-fields@v0.1.0 | |
| with: | |
| project-url: ${{ env.PROJECT_URL }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| item-id: ${{ steps.addp.outputs.itemId }} | |
| field-keys: Start date,Target date | |
| field-values: ${{ steps.derive.outputs.start_date }},${{ steps.derive.outputs.target_date }} | |
| - name: Add or update aging and SLA annotation | |
| if: steps.preflight.outputs.enabled == 'true' && github.event.action != 'closed' | |
| uses: actions/github-script@v7 | |
| env: | |
| SLA_WARN_DAYS: ${{ env.SLA_WARN_DAYS }} | |
| SLA_BREACH_DAYS: ${{ env.SLA_BREACH_DAYS }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const marker = "<!-- project-meta-sync:sla -->"; | |
| const isIssue = context.eventName === "issues"; | |
| const number = isIssue ? context.payload.issue.number : context.payload.pull_request.number; | |
| const createdAt = new Date( | |
| isIssue ? context.payload.issue.created_at : context.payload.pull_request.created_at, | |
| ); | |
| const now = new Date(); | |
| const ageDays = Math.floor((now - createdAt) / (1000 * 60 * 60 * 24)); | |
| const warnDays = Number(process.env.SLA_WARN_DAYS || "7"); | |
| const breachDays = Number(process.env.SLA_BREACH_DAYS || "14"); | |
| let slaState = "Within SLA"; | |
| if (ageDays >= breachDays) slaState = "SLA breached"; | |
| else if (ageDays >= warnDays) slaState = "SLA risk"; | |
| const body = `${marker} | |
| ## ⏱️ Aging and SLA annotation | |
| - Age: **${ageDays} day(s)** | |
| - SLA state: **${slaState}** | |
| - Thresholds: warn at ${warnDays} days, breach at ${breachDays} days | |
| - Last updated: ${now.toISOString()} | |
| _Maintained by project-meta-sync workflow._`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find( | |
| (c) => | |
| c.user?.type === "Bot" && | |
| typeof c.body === "string" && | |
| c.body.includes(marker), | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| core.info(`Updated SLA annotation on #${number}`); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: number, | |
| body, | |
| }); | |
| core.info(`Created SLA annotation on #${number}`); | |
| } |