From 5ffa7634feb9847ada6b6566a735f7261d1f4824 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Thu, 15 Oct 2026 19:14:11 +0000 Subject: [PATCH 1/3] ci: analyze source with SonarQube Cloud --- .github/workflows/sonar.yml | 31 +++++++++++++++++++++++++++++++ README.md | 2 ++ sonar-project.properties | 12 ++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .github/workflows/sonar.yml create mode 100644 sonar-project.properties diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..8aad74c --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,31 @@ +name: SonarQube + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: sonar-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analysis: + name: SonarQube analysis + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: -Dsonar.qualitygate.wait=true -Dsonar.qualitygate.timeout=300 diff --git a/README.md b/README.md index de32f59..d0734d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Durable Workflow AI +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=durable-workflow_ai&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=durable-workflow_ai) +

CI status Packagist version diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..5a9e92b --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,12 @@ +sonar.organization=durable-workflow +sonar.projectKey=durable-workflow_ai +sonar.projectName=ai +sonar.sourceEncoding=UTF-8 +sonar.sources=. +sonar.tests=. +sonar.test.inclusions=tests/**,**/tests/**,**/*.test.*,**/*.spec.*,**/test_*.py,benchmarks/**,scripts/conformance/** +# Dependencies, generated assets and disposable runtime/build output are not source. +sonar.exclusions=**/vendor/**,**/node_modules/**,**/target/**,**/dist/**,**/build/**,**/storage/**,**/.venv/**,**/__pycache__/**,**/coverage/**,**/.scannerwork/**,**/.git/**,static/vendor/**,public/vendor/**,resources/js/vendor/** +sonar.test.exclusions=**/vendor/**,**/node_modules/**,**/target/**,**/.venv/** +# Existing repository test and coverage gates remain required; do not fabricate reports. +sonar.sca.enabled=false From 350b7e8b8f2e7970d2332dbd3253c6a792e39107 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Thu, 15 Oct 2026 19:24:46 +0000 Subject: [PATCH 2/3] ci: keep uncollected coverage distinct from static analysis --- sonar-project.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 5a9e92b..71ac316 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,5 +8,7 @@ sonar.test.inclusions=tests/**,**/tests/**,**/*.test.*,**/*.spec.*,**/test_*.py, # Dependencies, generated assets and disposable runtime/build output are not source. sonar.exclusions=**/vendor/**,**/node_modules/**,**/target/**,**/dist/**,**/build/**,**/storage/**,**/.venv/**,**/__pycache__/**,**/coverage/**,**/.scannerwork/**,**/.git/**,static/vendor/**,public/vendor/**,resources/js/vendor/** sonar.test.exclusions=**/vendor/**,**/node_modules/**,**/target/**,**/.venv/** -# Existing repository test and coverage gates remain required; do not fabricate reports. +# This CI job performs static analysis. Repository test/coverage gates stay required. +# No coverage report is collected here; leave that metric unmeasured in Sonar. +sonar.coverage.exclusions=**/* sonar.sca.enabled=false From 28c611bc9cac51935f9df0f3b954ec815b7abe05 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Thu, 15 Oct 2026 19:46:25 +0000 Subject: [PATCH 3/3] fix: preserve E2B response validation after type narrowing --- src/Providers/E2bSandboxProvider.php | 4 +-- tests/Unit/E2bSandboxProviderTest.php | 39 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Providers/E2bSandboxProvider.php b/src/Providers/E2bSandboxProvider.php index 36ff858..0855d42 100644 --- a/src/Providers/E2bSandboxProvider.php +++ b/src/Providers/E2bSandboxProvider.php @@ -106,9 +106,7 @@ public function provision(array $options = []): SandboxHandle id: $id, provider: $this->name(), metadata: [ - 'template_id' => is_array($body) - ? (string) ($body['templateID'] ?? $this->templateId) - : $this->templateId, + 'template_id' => (string) ($body['templateID'] ?? $this->templateId), ], ); } diff --git a/tests/Unit/E2bSandboxProviderTest.php b/tests/Unit/E2bSandboxProviderTest.php index c9af17b..961d553 100644 --- a/tests/Unit/E2bSandboxProviderTest.php +++ b/tests/Unit/E2bSandboxProviderTest.php @@ -17,6 +17,7 @@ use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Request; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; use Throwable; @@ -64,6 +65,44 @@ public function test_provision_uses_documented_management_contract_and_bounded_t }); } + #[DataProvider('invalidProvisionResponses')] + public function test_provision_refuses_a_response_without_a_sandbox_identity(string $body): void + { + $this->http->fake([ + 'https://api.e2b.app/sandboxes' => $this->http->response($body, 201, [ + 'Content-Type' => 'application/json', + ]), + ]); + + $this->expectException(SandboxProvisionException::class); + $this->expectExceptionMessage('did not include sandboxID'); + $this->provider->provision(); + } + + /** @return array */ + public static function invalidProvisionResponses(): array + { + return [ + 'null' => ['null'], + 'scalar' => ['"unexpected"'], + 'missing identity' => ['{"templateID":"coding-agent"}'], + 'empty identity' => ['{"sandboxID":""}'], + ]; + } + + public function test_provision_retains_configured_template_when_response_omits_it(): void + { + $this->http->fake([ + 'https://api.e2b.app/sandboxes' => $this->http->response(['sandboxID' => 'sbx-default'], 201), + ]); + + $handle = $this->provider->provision(); + + $this->assertSame('sbx-default', $handle->id); + $this->assertSame('coding-agent', $handle->metadata['template_id']); + $this->http->assertSentCount(1); + } + public function test_provision_keeps_rate_limits_and_server_failures_retryable(): void { $statuses = [429, 500, 503];