name: Daily Product Intelligence on: workflow_dispatch: schedule: # 06:00 UTC daily - cron: "0 6 * * *" permissions: actions: read contents: read issues: write concurrency: group: product-intelligence-${{ github.ref }} cancel-in-progress: false jobs: report: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - name: Collect aggregate product signals id: collect continue-on-error: true env: GITHUB_TOKEN: ${{ github.token }} # SENTRY_PRODUCT_INTELLIGENCE_TOKEN is a dedicated read-only token and is # preferred if/when it exists; falls back to the general-purpose # SENTRY_AUTH_TOKEN so the daily cron doesn't silently no-op (#60). SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_PRODUCT_INTELLIGENCE_TOKEN || secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} run: | node scripts/product-intelligence.mjs \ --report "$RUNNER_TEMP/product-intelligence.md" \ --json "$RUNNER_TEMP/product-intelligence.json" - name: Create material-signal issue if: always() uses: actions/github-script@v8 env: REPORT_PATH: ${{ runner.temp }}/product-intelligence.json with: script: | const fs = require("fs") if (!fs.existsSync(process.env.REPORT_PATH)) { core.setFailed("Product intelligence collector produced no report.") return } const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, "utf8")) const allowedSignals = new Set([ "new-sentry-issue", "repeated-workflow-failure", ]) const validDate = typeof report.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(report.date) const validSignals = Array.isArray(report.signals) && report.signals.length > 0 && report.signals.every((signal) => allowedSignals.has(signal)) if (!report.material) { core.info("No material signal. No GitHub issue created.") return } if (!validDate || !validSignals) { core.setFailed("Collector report failed public issue allowlist validation.") return } const issues = await github.paginate(github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, state: "open", per_page: 100, }) const title = "Product intelligence: material signal" const marker = `` const body = [ marker, "", "## Material aggregate signal", "", `The daily product-intelligence run detected: ${[...report.signals].sort().join(", ")}.`, "", "This issue intentionally contains no raw diagnostic, review, request, or user-generated content. Review the sanitized Actions artifact and reproduce the behavior in the affected user channel before implementing a fix.", ].join("\n") const existing = issues.find((issue) => issue.title === title) if (existing) { await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: existing.number, title, body, }) core.info(`Updated #${existing.number}.`) return } const { data: issue } = await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title, body, labels: ["P1"], }) core.info(`Created #${issue.number}.`) - name: Upload sanitized report if: always() uses: actions/upload-artifact@v7 with: name: product-intelligence-${{ github.run_id }} path: | ${{ runner.temp }}/product-intelligence.md ${{ runner.temp }}/product-intelligence.json if-no-files-found: error - name: Fail when a required source is unavailable # scripts/product-intelligence.mjs only sets a non-zero exit code for # genuinely required-source failures (GitHub unavailable, or Sentry # unavailable for a reason other than the known missing/rejected # SENTRY_PRODUCT_INTELLIGENCE_TOKEN gap). A Sentry-token-missing run # is reported as "degraded" and this step does not fire for it — see # isSentryProvisioningGap() in the collector. This avoids the job # failing on its own known, human-gated gap and that failure being # counted as a "repeated-workflow-failure" product signal on a later # run (also guarded against directly via SELF_WORKFLOW_* exclusion # in the collector's workflow-failure count). if: steps.collect.outcome == 'failure' run: | echo "::error::A required product-intelligence source was unavailable. See the report summary for the exact source and error." exit 1