diff --git a/.github/agents/daily-code-review.agent.md b/.github/agents/daily-code-review.agent.md new file mode 100644 index 0000000..fbaa2ef --- /dev/null +++ b/.github/agents/daily-code-review.agent.md @@ -0,0 +1,198 @@ +--- +name: daily-code-review +description: >- + Autonomous daily code review agent that finds bugs, missing tests, and small + improvements in the DurableTask JavaScript SDK, then opens PRs with fixes. +tools: + - read + - search + - editFiles + - runTerminal + - github/issues + - github/issues.write + - github/pull_requests + - github/pull_requests.write + - github/search + - github/repos.read +--- + +# Role: Daily Autonomous Code Reviewer & Fixer + +## Mission + +You are an autonomous GitHub Copilot agent that reviews the DurableTask JavaScript SDK codebase daily. +Your job is to find **real, actionable** problems, fix them, and open PRs — not to generate noise. + +Quality over quantity. Every PR you open must be something a human reviewer would approve. + +## Repository Context + +This is a TypeScript monorepo for the Durable Task JavaScript SDK: + +- `packages/durabletask-js/` — Core orchestration SDK (`@microsoft/durabletask-js`) +- `packages/durabletask-js-azuremanaged/` — Azure Managed (DTS) backend (`@microsoft/durabletask-js-azuremanaged`) +- `examples/` — Sample applications +- `tests/` and `test/` — Unit and end-to-end tests +- `internal/protocol/` — Protobuf definitions + +**Stack:** TypeScript, Node.js (>=22), gRPC, Protocol Buffers, Jest, ESLint, npm workspaces. + +## Step 0: Load Repository Context (MANDATORY — Do This First) + +Read `.github/copilot-instructions.md` before doing anything else. It contains critical +architectural knowledge about this codebase: the replay execution model, determinism +invariants, task hierarchy, entity system, error handling patterns, and where bugs +tend to hide. This context is essential for distinguishing real bugs from intentional +design decisions. + +## Step 1: Deduplication (MANDATORY — Do This Second) + +Before analyzing any code, you MUST check what's already in-flight: + +1. **List all open PRs** with the `copilot-finds` label. +2. **List all open issues** with the `copilot-finds` label. +3. **List all open PRs** created by `copilot` or `github-actions[bot]` in the last 30 days. +4. **List recently merged PRs** (last 14 days) to avoid re-raising recently fixed items. + +Build an **exclusion list** of: +- File paths already touched by open PRs +- Problem descriptions already covered by open issues +- Areas recently fixed by merged PRs + +**Hard rule:** Never create a PR that overlaps with anything on the exclusion list. +If a finding is even partially covered by an existing issue or PR, skip it entirely. + +## Step 2: Code Analysis + +Scan the **entire repository** looking for these categories (in priority order): + +### Category A: Bugs (Highest Priority) +- Incorrect error handling (swallowed errors, missing try/catch, wrong error types) +- Race conditions or concurrency issues in async code +- Off-by-one errors, incorrect boundary checks +- Null/undefined dereference risks not guarded by types +- Logic errors in orchestration/entity state management +- Incorrect Promise handling (missing await, unhandled rejections) +- Resource leaks (unclosed streams, connections, timers) + +### Category B: Missing Tests +- Public API methods with zero or insufficient test coverage +- Edge cases not covered (empty inputs, error paths, boundary values) +- Recently added code paths with no corresponding tests +- Error handling branches that are never tested + +### Category C: Small Improvements +- Type safety gaps (implicit `any`, missing return types on public APIs) +- Dead code that can be safely removed +- Obvious performance issues (unnecessary allocations in hot paths) +- Missing input validation on public-facing functions + +### What NOT to Report +- Style/formatting issues (prettier/eslint handles these) +- Opinions about naming conventions +- Large architectural refactors +- Anything requiring domain knowledge you don't have +- Generated code (proto/, grpc generated files) +- Speculative issues ("this might be a problem if...") + +## Step 3: Rank and Select Findings + +From all findings, select the **single most impactful** based on: + +1. **Severity** — Could this cause data loss, incorrect behavior, or crashes? +2. **Confidence** — Are you sure this is a real problem, not a false positive? +3. **Fixability** — Can you write a correct, complete fix with tests? + +**Discard** any finding where: +- Confidence is below 80% +- The fix would be speculative or incomplete +- You can't write a meaningful test for it +- It touches generated code or third-party dependencies + +## Step 4: Create PR (1 Maximum) + +For each selected finding, create a **separate PR** with: + +### Branch Naming +`copilot-finds//` where category is `bug`, `test`, or `improve`. + +Example: `copilot-finds/bug/fix-unhandled-promise-rejection` + +### PR Content + +**Title:** `[copilot-finds] : ` + +**Body must include:** +1. **Problem** — What's wrong and why it matters (with file/line references) +2. **Root Cause** — Why this happens +3. **Fix** — What the PR changes and why this approach +4. **Testing** — What new tests were added and what they verify +5. **Risk** — What could go wrong with this change (be honest) + +### Code Changes +- Fix the actual problem +- Add new test(s) that: + - Would have caught the bug (for bug fixes) + - Cover the previously uncovered path (for missing tests) + - Verify the improvement works (for improvements) +- Keep changes minimal and focused — one concern per PR + +### Labels +Apply the `copilot-finds` label to every PR. + +## Step 5: Quality Gates (MANDATORY — Do This Before Opening Each PR) + +Before opening each PR, you MUST: + +1. **Run the full test suite:** + ```bash + npm install + npm run build + npm run test:unit + ``` + +2. **Run linting:** + ```bash + npm run lint + ``` + +3. **Verify your new tests pass:** + - Your new tests must be in the appropriate test directory + - They must follow existing test patterns and conventions + - They must actually test the fix (not just exist) + +**If any tests fail or lint errors appear:** +- Fix them if they're caused by your changes +- If pre-existing failures exist, note them in the PR body but do NOT let your changes add new failures +- If you cannot make tests pass, do NOT open the PR — skip to the next finding + +## Behavioral Rules + +### Hard Constraints +- **Maximum 1 PR per run.** Pick only the single highest-impact finding. +- **Never modify generated files** (`*_pb.js`, `*_pb.d.ts`, `*_grpc_pb.js`, proto files). +- **Never modify CI/CD files** (`.github/workflows/`, `eng/`, `azure-pipelines.yml`). +- **Never modify package.json** version fields or dependency versions. +- **Never introduce new dependencies.** +- **If you're not sure a change is correct, don't make it.** + +### Quality Standards +- Match the existing code style exactly (indentation, quotes, naming patterns). +- Use the same test patterns the repo already uses (Jest, describe/it blocks). +- Write test names that clearly describe what they verify. +- Prefer explicit assertions over snapshot tests. + +### Communication +- PR descriptions must be factual, not promotional. +- Don't use phrases like "I noticed" or "I found" — state the problem directly. +- Acknowledge uncertainty: "This fix addresses X; however, the broader pattern in Y may warrant further review." +- If a fix is partial, say so explicitly. + +## Success Criteria + +A successful run means: +- 0-1 PRs opened, with a real fix and new tests +- Zero false positives +- Zero overlap with existing work +- All tests pass +- A human reviewer can understand and approve within 5 minutes diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c8c1501 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,342 @@ +# Copilot Instructions — DurableTask JavaScript SDK + +This document provides architectural context for AI assistants working with this codebase. +Focus is on **stable patterns, invariants, and pitfalls** — not file paths or function signatures. + +--- + +## What This Project Is + +The **Durable Task SDK for JavaScript/TypeScript** enables writing long-running, stateful +workflows as code. It is the JavaScript implementation of Microsoft's cross-language +Durable Task Framework (sibling SDKs exist in .NET, Python, Java, Go). + +**This is NOT Azure Durable Functions.** It's a lower-level SDK for the Azure Durable Task +Scheduler service (or its emulator). The two are related but have different APIs, deployment +models, and npm packages. + +--- + +## Core Execution Model — Generator-Based Replay + +This is the single most important thing to understand. Everything else follows from it. + +### How Orchestrations Work + +Orchestrations are **async generator functions** (`async function*`) that `yield` `Task` +objects. The runtime replays completed history events to rebuild state, then advances the +generator for new work. + +``` +Orchestrator yields Task → Runtime checks history → Already done? Send result back + → Not done? Suspend generator, create action for sidecar +``` + +On every re-invocation: +1. The executor receives `(instanceId, oldEvents[], newEvents[])`. +2. With `isReplaying = true`: feeds `oldEvents` through event processing. Completed tasks + resolve instantly via `generator.next(result)`. +3. With `isReplaying = false`: processes `newEvents`. Incomplete tasks suspend the generator. +4. The resulting `OrchestratorAction[]` are sent back to the sidecar. + +### The Determinism Rule (Critical) + +**Orchestrator code MUST be deterministic.** Every replay must produce the exact same +sequence of actions. This is enforced at runtime — mismatches throw `NonDeterminismError`. + +What this means in practice: +- **No `Date.now()`** — use `context.currentUtcDateTime` +- **No `Math.random()` or `crypto.randomUUID()`** — use `context.newGuid()` (deterministic UUID v5) +- **No direct I/O** (HTTP calls, file reads, database queries) — use activities +- **No non-deterministic control flow** — no data-dependent branching on external state + +Adding, removing, or reordering `yield` statements between code versions **breaks replay** +for in-flight orchestrations. The validator catches type/name mismatches but not all +ordering issues. + +### Activities vs Orchestrations + +Activities are the leaf nodes — they execute side effects exactly once (modulo retries) +and persist results as history events. They can do anything: HTTP calls, DB writes, etc. + +**Key mental model:** Orchestrations = coordination logic (deterministic). +Activities = real work (non-deterministic allowed). + +--- + +## Architecture — How Pieces Connect + +### Communication Model + +The SDK **never touches storage directly**. All communication happens over gRPC to a +sidecar (Azure Durable Task Scheduler, emulator, or Dapr): + +``` +Client → gRPC → Sidecar (owns storage & scheduling) + ↕ +Worker ← gRPC (streaming) ← Sidecar pushes work items +``` + +The worker opens a **server-streaming RPC** (`getWorkItems`) and dispatches incoming work: +orchestration replays, activity executions, and entity operations. + +### Package Structure + +This is an npm workspaces monorepo with two published packages: + +| Package | Role | +|---|---| +| Core SDK | Orchestration engine, client, worker, entities, testing, tracing | +| Azure Managed Backend | Connection string / Entra ID auth, credential factory, builder pattern | + +The Azure package **peers on** the core package — it adds authentication and connection +management but delegates all domain logic to core. + +### Protocol Layer + +Protobuf definitions live in `internal/protocol/`. Generated JavaScript stubs (not +TypeScript) provide the gRPC message types. A helper layer converts between protobuf +messages and SDK types (history events, orchestrator actions). + +**Generated proto files should never be manually edited.** They're rebuilt from the proto +definitions using `grpc_tools_node_protoc`. + +--- + +## Task System — The Heart of the SDK + +### Task Hierarchy + +``` +Task — Base: result, exception, isComplete, isFailed, parent ref +├── CompletableTask — Can be completed or failed; notifies parent on completion +│ ├── RetryableTask — Policy-driven retries (exponential backoff) +│ └── RetryHandlerTask — Custom retry handler function +└── CompositeTask — Holds children with parent backlinks + ├── WhenAllTask — Completes when ALL children done; fails fast on first failure + └── WhenAnyTask — Completes when ANY child done +``` + +**Parent notification chain:** When a child task completes, it calls +`parent.onChildCompleted()`. This chain drives composite task completion. + +**Important:** `CompositeTask` checks already-complete children in its constructor. +A composite task can be **immediately complete upon construction** — callers must handle this. + +### WhenAll Fail-Fast Behavior + +`WhenAllTask` marks itself complete on the **first** failed child. Other children may still +be in flight. The `isComplete` guard prevents double-completion, but the mental model is: +one failure = whole task fails immediately, remaining results ignored. + +--- + +## Entity System + +### Identity Model + +Entities are addressed as `@name@key`. Names are **always lowercased** (cross-SDK +consistency with .NET). Keys **preserve case**. + +### Execution Model + +Entities process operations in **single-threaded batches** with per-operation transactional +semantics: +1. Checkpoint state + accumulated actions before each operation +2. Execute the operation +3. On success: commit. On failure: rollback to checkpoint. + +State uses **lazy JSON deserialization** — deserialize only on first read. If state is +written but never read, the original JSON is discarded (intentional optimization). + +### Method Dispatch + +`TaskEntity` does automatic method dispatch: it walks the prototype chain, finds +a method matching the operation name (case-insensitive), and calls it. The implicit +`delete` operation sets state to `null`. + +### Entity Lock Ordering + +`lockEntities()` **sorts entity IDs** before acquiring locks to prevent deadlocks. +This is enforced in the orchestration context — never bypass it or implement custom locking. + +--- + +## Error Handling Patterns + +### Exception Hierarchy + +| Error | When | +|---|---| +| `TaskFailedError` | Activity or sub-orchestration fails (carries `FailureDetails`) | +| `NonDeterminismError` | Replayed action mismatches history (not recoverable) | +| `OrchestrationStateError` | Invalid state transition attempted | +| `EntityOperationFailedException` | Entity operation throws | +| `TimeoutError` | Wait timeout exceeded | + +### Error Propagation Flow + +1. Activity throws → sidecar records failure event → executor calls `task.fail()` → + generator receives thrown `TaskFailedError` → orchestrator can catch or propagate. +2. Uncaught orchestrator error → executor catches → creates complete action with + failure details → sidecar records failed status. +3. Non-determinism → immediately throws, aborts replay. **Not recoverable by user code.** + +### Retry System + +Two flavors, both using **durable timers** for backoff (survives process restarts): +- **Policy-driven:** `RetryPolicy` with max attempts, backoff coefficient, intervals. + Delay formula: `firstRetryInterval × backoffCoefficient^(attempt - 1)`, capped. +- **Custom handler:** User function receives `RetryContext`, returns `RetryAction`. + +**Critical invariant:** Retry tasks create timers whose `taskId` differs from the original +task's ID. The executor maintains maps to link timer IDs back to retry tasks. If this +linkage breaks (e.g., during continue-as-new), retries silently fail. + +--- + +## Tracing (OpenTelemetry) + +OpenTelemetry is an **optional peer dependency**, loaded via `require()` with fallback. +If not available, tracing is silently disabled. + +Cross-SDK conventions (shared with .NET, Python, Java, Go): +- Tracer name: `"Microsoft.DurableTask"` +- Span naming: `"{type}:{name}"` (e.g., `"orchestration:MyWorkflow"`) +- W3C `traceparent` propagated via protobuf `TraceContext` fields +- Replay spans carry forward original span ID for cross-replay correlation + +--- + +## Logging Conventions + +### Logger Interface + +The SDK supports two logging modes, detected at runtime via type guard: +- **Plain `Logger`:** `error()`, `warn()`, `info()`, `debug()` — string messages +- **`StructuredLogger`:** Adds `logEvent(level, event, message)` with event IDs and + categories matching the .NET SDK for cross-SDK log correlation + +### Replay-Safe Logging + +`ReplaySafeLogger` wraps any logger and **suppresses all output when `isReplaying = true`**. +This prevents duplicate log entries during history replay. Always use it in orchestrator +context. + +--- + +## Testing Approach + +### In-Memory Backend + +The SDK provides a full in-memory testing stack that mirrors the gRPC path: + +``` +TestOrchestrationClient → InMemoryOrchestrationBackend ← TestOrchestrationWorker +``` + +This enables testing orchestrations with the **real executor logic** but no sidecar, no +gRPC, no network. The backend manages instance state, work queues, timers, and state +waiters. + +Key test utilities: +- `backend.waitForState(predicate, timeout)` — polling with timeout for async assertions +- `backend.reset()` — clear all state between tests +- Direct timer control for deterministic time-dependent tests + +### Test Conventions + +- Framework: Jest 29 with `ts-jest` +- File naming: `.spec.ts` suffix +- Protobuf test helpers: factory functions create `HistoryEvent` instances without a sidecar +- Three tiers: unit tests (no sidecar), e2e with sidecar, e2e with Azure + +--- + +## Where Bugs Tend to Hide + +These are stable architectural areas where complexity concentrates: + +1. **Replay event matching** — The executor's large switch statement processes 27+ event + types. Unmatched events are logged but silently dropped in several places. + The TODOs in the code explicitly question whether these should be errors. + +2. **Timer-to-retry linkage** — Retry tasks create timers with different IDs. The maps + connecting them are critical. Any break in linkage = silent retry failure. + +3. **Generator lifecycle edge cases** — What happens when the generator yields `null`? + When `done` is true but the loop keeps checking? The initial `next()` return value? + Several TODOs mark these as not fully hardened. + +4. **Composite task constructor side effects** — Already-complete children trigger + `onChildCompleted()` during construction, potentially completing the composite + before the caller inspects it. + +5. **Continue-as-new state reset** — Resetting history while preserving carryover events + (buffered external events) is a subtle operation. Incorrect carryover handling leaks + state between iterations. + +6. **Suspend/resume event buffering** — Suspended orchestrations buffer "suspendable" + events. On resume, all buffered events are replayed in order. If events arrive at + the boundary of suspend/resume transitions, ordering may be surprising. + +7. **gRPC stream lifecycle** — The worker's streaming connection can enter states where + it's neither cleanly closed nor cleanly connected. The reconnection logic handles most + cases, but simultaneous close + reconnect races exist. + +8. **Entity V1 vs V2 code paths** — The worker supports both entity execution paths. + V2 is current; V1 is legacy. Incorrect version detection or mixed-version scenarios + could cause issues. + +--- + +## Code Conventions + +### Naming +- Files: `kebab-case.ts` +- Classes: `PascalCase` +- Private fields: `_underscorePrefixed` +- Type aliases: `T`-prefixed (`TOrchestrator`, `TActivity`) +- Constants: `UPPER_SNAKE_CASE` or `PascalCase` objects with `UPPER_SNAKE_CASE` properties +- Enums: `PascalCase` names with `PascalCase` members + +### Module System +- CommonJS output, ES2022 target +- OpenTelemetry loaded via synchronous `require()` with catch (future ESM migration noted) + +### Cross-SDK Consistency +Many decisions mirror the .NET SDK intentionally: entity name lowercasing, structured +log event IDs, tracing attribute names, span naming conventions. When in doubt about +"why is it done this way?", the answer is often ".NET SDK compatibility." + +### License +Every source file starts with the Microsoft copyright header + MIT license reference. + +### Builder Pattern +Both packages use the builder pattern for constructing clients and workers. +Fluent API with `.host()`, `.port()`, `.connectionString()`, `.endpoint()`, `.build()`. + +### Registration Lock +`addOrchestrator()`, `addActivity()`, `addEntity()` all throw if called after `start()`. +The registry is frozen once the worker begins processing. + +--- + +## What Not to Touch + +- **Generated proto files** (`*_pb.js`, `*_pb.d.ts`, `*_grpc_pb.js`) — regenerated from + proto definitions, never manually edited +- **`version.ts`** — auto-generated by prebuild script +- **Proto definitions** in `internal/protocol/` — shared across language SDKs, changes + must be coordinated + +--- + +## Key Design Constraints to Respect + +1. **No new dependencies** without careful consideration — this is a core SDK +2. **Node.js >= 22 required** — the engines field enforces this +3. **Single-threaded execution** — no locking primitives; correctness depends on not + yielding to the event loop during replay processing +4. **Pre-1.0 API** — public API surface is still evolving but changes should be deliberate +5. **Cross-SDK alignment** — behavior should match .NET/Python/Java/Go SDKs where applicable diff --git a/.github/workflows/daily-code-review.yaml b/.github/workflows/daily-code-review.yaml new file mode 100644 index 0000000..bc540dc --- /dev/null +++ b/.github/workflows/daily-code-review.yaml @@ -0,0 +1,238 @@ +name: 🔍 Daily Code Review Agent + +on: + # Run every day at 08:00 UTC + schedule: + - cron: "0 8 * * *" + # Allow manual trigger for testing + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + daily-code-review: + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + NODE_VER: "22" + + steps: + - name: 📥 Checkout code (full history for better analysis) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ⚙️ Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VER }} + + - name: 📦 Install dependencies + run: npm ci + + - name: 🔨 Build project + run: npm run build + + - name: 🔍 Collect existing work to avoid duplicates + id: dedup + run: | + echo "Fetching open PRs and issues with copilot-finds label..." + + # Get open PRs with copilot-finds label (--limit ensures we don't miss any) + OPEN_PRS=$(gh pr list \ + --label "copilot-finds" \ + --state open \ + --limit 200 \ + --json title,url,headRefName,files \ + --jq '[.[] | {title: .title, url: .url, branch: .headRefName, files: [.files[].path]}]' \ + 2>/dev/null || echo "[]") + + # Get open issues with copilot-finds label + OPEN_ISSUES=$(gh issue list \ + --label "copilot-finds" \ + --state open \ + --limit 200 \ + --json title,url,body \ + --jq '[.[] | {title: .title, url: .url}]' \ + 2>/dev/null || echo "[]") + + # Get recently merged PRs (last 14 days) with copilot-finds label + # Use jq numeric timestamp comparison (fromdateiso8601) to avoid string/timezone issues + RECENT_MERGED=$(gh pr list \ + --label "copilot-finds" \ + --state merged \ + --limit 200 \ + --json title,url,mergedAt,files \ + --jq '[.[] | select((.mergedAt | fromdateiso8601) > (now - 14*86400)) | {title: .title, url: .url, files: [.files[].path]}]' \ + 2>/dev/null || echo "[]") + + # Get all open PRs by bots + BOT_PRS=$(gh pr list \ + --author "app/github-actions" \ + --state open \ + --limit 200 \ + --json title,url,headRefName \ + --jq '[.[] | {title: .title, url: .url, branch: .headRefName}]' \ + 2>/dev/null || echo "[]") + + # Combine into exclusion context + EXCLUSION_CONTEXT=$(cat < /tmp/exclusion-context.txt + + echo "Dedup context collected:" + echo "- Open copilot-finds PRs: $(echo "$OPEN_PRS" | jq 'length')" + echo "- Open copilot-finds issues: $(echo "$OPEN_ISSUES" | jq 'length')" + echo "- Recently merged: $(echo "$RECENT_MERGED" | jq 'length')" + echo "- Bot PRs: $(echo "$BOT_PRS" | jq 'length')" + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + + - name: ✅ Verify tests pass before analysis + run: | + npm run test:unit || echo "::warning::Some pre-existing unit test failures detected" + + - name: 🏷️ Ensure copilot-finds label exists + run: | + gh label create "copilot-finds" \ + --description "Findings from daily automated code review agent" \ + --color "7057ff" \ + --force + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + + - name: 🤖 Install GitHub Copilot CLI + run: npm install -g @github/copilot + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + + - name: 🔍 Run Daily Code Review Agent + run: | + EXCLUSION_CONTEXT=$(cat /tmp/exclusion-context.txt) + AGENT_PROMPT=$(cat .github/agents/daily-code-review.agent.md) + + FULL_PROMPT=$(cat <&1 || EXIT_CODE=$? + + if [ $EXIT_CODE -eq 124 ]; then + echo "::warning::Agent timed out after 20 minutes" + elif [ $EXIT_CODE -ne 0 ]; then + echo "::warning::Agent exited with code $EXIT_CODE" + fi + + echo "Daily code review agent completed." + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + CI: "true" + NO_COLOR: "1" + TERM: "dumb" + + - name: 📊 Summary + if: always() + run: | + echo "## Daily Code Review Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Date:** $(date +%Y-%m-%d)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count PRs created in this run using numeric timestamp comparison + CUTOFF_EPOCH=$(date -d '1 hour ago' +%s) + PR_COUNT=$(gh pr list \ + --label "copilot-finds" \ + --state open \ + --limit 200 \ + --json createdAt \ + --jq "[.[] | select((.createdAt | fromdateiso8601) > $CUTOFF_EPOCH)] | length" \ + 2>/dev/null || echo "0") + + echo "**PRs opened this run:** $PR_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$PR_COUNT" -gt 0 ]; then + echo "### New PRs:" >> $GITHUB_STEP_SUMMARY + gh pr list \ + --label "copilot-finds" \ + --state open \ + --limit 200 \ + --json title,url,createdAt \ + --jq ".[] | select((.createdAt | fromdateiso8601) > $CUTOFF_EPOCH) | \"- [\(.title)](\(.url))\"" \ + 2>/dev/null >> $GITHUB_STEP_SUMMARY || true + else + echo "_No new findings today — codebase looking good! 🎉_" >> $GITHUB_STEP_SUMMARY + fi + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }}