# Sonar Quality Gates

Open Orchestra uses Sonar as a repository and SaaS project-quality signal. It
does not replace secret scanning or runtime policy enforcement.

## Repo Audit

The repository includes `sonar-project.properties` and a dedicated GitHub
Actions workflow at `.github/workflows/sonar.yml`.

Supported provider modes:

- `sonarcloud`: hosted SonarQube Cloud. Best for quick setup and public or
  small repositories where hosted analysis is acceptable.
- `sonarqube-local`: local SonarQube for development and private-repo
  dogfooding. The default local host is `http://localhost:9000`.
- `sonarqube-self-hosted`: organization-managed SonarQube Server. Use this for
  regulated tenants, private codebases, or repositories where hosted LOC limits
  and external API permissions are a concern.

### Community Branch and PR Decoration Option

SonarQube Community Edition does not include the same branch analysis and pull
request decoration capabilities as paid editions. If those capabilities are
needed in a private local or self-hosted Community setup, evaluate
[`mc1arke/sonarqube-community-branch-plugin`](https://github.com/mc1arke/sonarqube-community-branch-plugin)
as an optional infrastructure add-on.

Use this only as an explicitly accepted operational dependency:

- It is not maintained or supported by SonarSource.
- Plugin compatibility follows the SonarQube major/minor version; pin the image
  or plugin release to the running SonarQube version.
- Migration from Community Edition plus this plugin to commercial SonarQube
  editions has no guaranteed official upgrade path.
- SaaS or regulated-tenant usage requires a separate security, supportability,
  upgrade, backup, and rollback review before adoption.

For low-cost local dogfooding, prefer the plugin's published Docker image or a
separate shared SonarQube infrastructure repository instead of coupling plugin
installation to this product repository.

Required GitHub secret when the GitHub Actions workflow is enabled:

- `SONAR_TOKEN`: token for SonarQube Cloud or SonarQube Server.

Optional GitHub secrets:

- `SONAR_HOST_URL`: required for self-hosted SonarQube Server. Leave unset for
  SonarQube Cloud, or set `http://localhost:9000` only for local commands.
- `CF_ACCESS_CLIENT_ID` and `CF_ACCESS_CLIENT_SECRET`: Cloudflare Access service
  token credentials for GitHub-hosted runners that must reach a private
  self-hosted SonarQube URL protected by Zero Trust.

Optional GitHub variables:

- `SONAR_PROVIDER`: `sonarcloud`, `sonarqube-local`, or
  `sonarqube-self-hosted`. GitHub-hosted runners normally use `sonarcloud` or a
  reachable self-hosted server; local SonarQube is intended for local commands.
- `SONAR_CLOUD_ENABLED`: set to `true` to run SonarCloud automatically on push
  and pull request events. When unset, SonarCloud runs only through manual
  `workflow_dispatch`.
- `SONAR_QUALITY_GATE_WAIT`: set to `true` to fail the workflow when the remote
  quality gate fails.
- `SONAR_RUNNER`: set to `self-hosted` to run the Sonar workflow on a local
  runner that can reach the shared SonarQube runtime directly. When this is set,
  the workflow resolves `SONAR_LOCAL_HOST_URL`, then `SONAR_HOST_URL`, then
  `http://localhost:9001`. Cloudflare Access service-token checks still run
  when the resolved URL uses `SONAR_HOST_URL` and Cloudflare credentials are
  configured.
- `SONAR_LOCAL_HOST_URL`: optional override for self-hosted runner mode when the
  runner reaches SonarQube through a different local-only URL. Prefer this over
  `SONAR_HOST_URL` when the self-hosted runner should use a direct local path.

The workflow skips analysis when `SONAR_TOKEN` is not configured. This keeps
forks and offline development usable. For private repositories, keep
`SONAR_CLOUD_ENABLED` unset unless hosted analysis is intentionally approved.

The workflow supports remote quality gate enforcement when the repository
variable `SONAR_QUALITY_GATE_WAIT=true` is configured. In that mode the scanner
runs with `sonar.qualitygate.wait=true` and `sonar.qualitygate.timeout=300`. A
failed quality gate fails the GitHub Actions job instead of reporting only a
successful scanner upload.

The token used for this mode must be able to read the Sonar project and quality
gate status. If the scanner can upload analysis but the wait step fails with
`Project not found`, update the `SONAR_TOKEN` permissions or keep
`SONAR_QUALITY_GATE_WAIT` unset until the token can read the project.

Before scanner execution, CI runs:

```bash
node bin/orchestra.js sonar preflight \
  --provider "$SONAR_PROVIDER_RESOLVED" \
  --project-key jterrats_open-orchestra \
  --organization jterrats \
  --host-url "$SONAR_HOST_URL_RESOLVED" \
  --branch "$BRANCH_NAME"
```

The preflight validates token authentication, project visibility, quality gate
API access, issue API access, and security hotspot API access. It redacts the
token, host URL, and Cloudflare Access service token values from diagnostic
output. `hotspots` is a warning when unavailable because some Sonar tokens can
analyze and read issues while hotspot review permissions are managed separately.
In local `sonarqube-local` mode, a branch-scoped quality gate `404` falls back
to the default branch when the project itself is readable, because SonarQube
Community Edition does not expose branch analysis unless an optional add-on or
commercial feature is installed.

Common remediation:

- `auth-invalid`: regenerate `SONAR_TOKEN`; do not paste header names or
  prefixes into the secret value.
- `project-not-found`: verify the project key and organization, then grant the
  token user Browse access on the project.
- `permission-denied`: grant Execute Analysis for scanner upload and Browse/API
  access for evidence import. Use a user token when hotspot APIs must be read.
- `cloudflare-challenge`: use the Cloudflare Access service token proxy, a
  self-hosted runner with local `SONAR_LOCAL_HOST_URL`, or an Access policy that
  explicitly allows the CI service token without an interactive challenge.

## Local SonarQube

Open Orchestra does not own the long-lived local SonarQube containers. The
shared laptop/VPS runtime lives in `~/dev/sonarqube_jeterrats_dev` so multiple
projects can use the same SonarQube server without tying its lifecycle to this
repository.

```bash
cd ~/dev/sonarqube_jeterrats_dev
docker compose up -d
```

The shared runtime binds SonarQube to `127.0.0.1:${SONAR_PORT:-9001}` by
default, persists data in Docker volumes, and routes
`sonarqube.jterrats.dev` through the Cloudflare Tunnel named
`open-orchestra-sonar-local`.
The local database password is a rotated strong value stored only in the shared
infra `.env` file with owner-only file permissions; do not reset it to the
default `sonar` password.

```bash
cd ~/dev/sonarqube_jeterrats_dev
docker compose ps
docker compose logs -f sonarqube
docker compose logs -f cloudflared
```

This repository keeps only project-specific assets: `sonar-project.properties`,
scanner scripts, import commands, and release evidence. That separation avoids
one project accidentally stopping, deleting, changing ports, or rotating
credentials for every other project using the same SonarQube server.

Open `http://localhost:9001`, complete the SonarQube first-run setup if needed,
create the `jterrats_open-orchestra` project key, and generate a project token.
The scanner and `orchestra sonar import` both authenticate with the token as
SonarQube Basic auth (`<token>:`), so the token must be valid for analysis and
API reads on the target project. Then run scanner/import commands against the
local or tunnel host. Example local scan:

```bash
SONAR_HOST_URL=http://localhost:9001 SONAR_TOKEN=<local-token> npm run sonar:scan:local
```

Example import after analysis is available:

```bash
SONAR_TOKEN=<local-token> node bin/orchestra.js sonar import \
  --provider sonarqube-local \
  --host-url http://localhost:9001 \
  --project-key jterrats_open-orchestra \
  --branch main \
  --task GH-368-LOCAL-SONARQUBE-PROVIDER \
  --json
```

HTTP is accepted only for `sonarqube-local` on localhost. Shared tunnel and
cloud hosts must use HTTPS.

### Private Cloudflare Access

Do not expose SonarQube as a public DNS-only origin. If remote access is needed,
use Cloudflare Tunnel with Cloudflare Access so `sonarqube.jterrats.dev` is an
authenticated private entry point, not an open public service.

Minimum Cloudflare setup:

- Create a tunnel for this laptop or temporary VPS. The current tunnel name is
  `open-orchestra-sonar-local`.
- Route `sonarqube.jterrats.dev` to the Sonar service behind the tunnel. The DNS
  record is a proxied CNAME to
  `6fb60222-1427-4ca1-bf11-9e19375d39ff.cfargotunnel.com`.
- Protect the hostname with a Cloudflare Access self-hosted application.
- Restrict Access to named users, groups, or a short-lived maintainer policy.
- Require MFA at the identity provider when possible.
- Keep SonarQube itself authenticated; Cloudflare Access is an outer gate, not a
  replacement for Sonar users and tokens.

When the tunnel hostname is active, CI can use
`SONAR_HOST_URL=https://sonarqube.jterrats.dev` only if the runner has an
approved Access path. For GitHub-hosted runners, create a Cloudflare Access
service token, add a Service Auth policy scoped to the SonarQube application,
and configure `CF_ACCESS_CLIENT_ID` plus `CF_ACCESS_CLIENT_SECRET` as GitHub
secrets. The workflow starts an ephemeral localhost proxy that injects those
headers for SonarScanner and Orchestra import calls; browser login remains
required for human access. The proxy readiness check validates SonarQube through
the configured `SONAR_TOKEN` so private SonarQube instances that require
authentication do not fail on anonymous health endpoints.

If the Access service token secrets are not configured, the workflow keeps the
normal direct Sonar URL behavior. Use local analysis evidence or a self-hosted
runner when GitHub-hosted runners cannot access the private endpoint.

### Self-Hosted Runner Mode

For private local SonarQube on a laptop or low-cost VPS, prefer a self-hosted
GitHub Actions runner over exposing the analyzer path through Cloudflare Access.
Configure the repository or organization variable:

```bash
gh variable set SONAR_RUNNER --repo jterratsdev/open-orchestra --body self-hosted
```

Register the runner with dedicated labels so only the Sonar job can claim it.
Do not include OS-specific labels in the workflow unless the Sonar runtime truly
depends on that operating system; this keeps the same CI definition usable from
a macOS laptop today and a Linux host later.

```text
self-hosted
sonar
local-sonar
```

For the current laptop setup, use the macOS ARM64 runner package and configure
the runner with `--labels sonar,local-sonar`. GitHub automatically adds the
platform labels such as `self-hosted`, `macOS`, and `ARM64`.

Current macOS ARM64 bootstrap:

```bash
mkdir -p ~/dev/actions-runner-open-orchestra-sonar
cd ~/dev/actions-runner-open-orchestra-sonar
curl -L -o actions-runner-osx-arm64.tar.gz <github-runner-osx-arm64-download-url>
tar xzf actions-runner-osx-arm64.tar.gz
./config.sh --url https://github.com/jterratsdev/open-orchestra --labels sonar,local-sonar --name open-orchestra-sonar-macos-arm64
./run.sh
```

Future Linux runner bootstrap should use the Linux package that matches the host
architecture, the same `sonar,local-sonar` labels, and a dedicated service
account. Do not reuse a broad organization runner for Sonar unless the runner
already has least-privilege filesystem, Docker, and network access.

Service lifecycle:

```bash
cd ~/dev/actions-runner-open-orchestra-sonar
./svc.sh install
./svc.sh start
./svc.sh status
./svc.sh stop
```

Use `./run.sh` for interactive local troubleshooting. Use `svc.sh` only after
the runner can start, claim a Sonar workflow job, and reach SonarQube locally.

Keep the shared SonarQube stack running locally:

```bash
cd ~/dev/sonarqube_jterrats_dev
docker compose up -d
```

When `SONAR_RUNNER=self-hosted`, the workflow resolves SonarQube in this order:
`SONAR_LOCAL_HOST_URL`, `SONAR_HOST_URL`, then `http://localhost:9001`.
Use `SONAR_LOCAL_HOST_URL` to force a direct local path and avoid accidentally
pulling local machine analysis back through Zero Trust. When no local URL is
set and `SONAR_HOST_URL` points at a Cloudflare Access protected hostname, the
workflow enables the service-token proxy before preflight. The CI scan uses
`continue-on-error` on the scanner step so Orchestra can still import and upload
Sonar evidence when the quality gate fails; a final workflow step re-fails the
job after evidence is captured.

If the runner itself runs inside a container, `localhost` points at the runner
container. In that case either run the runner process on the host, attach the
runner container to the SonarQube Docker network, or set `SONAR_LOCAL_HOST_URL`
to the host/network address that reaches SonarQube without Cloudflare.

### Self-Hosted Runner Health Check

Before relying on CI, validate the local runner and SonarQube path from the same
machine that runs the GitHub Actions runner:

```bash
gh variable list --repo jterratsdev/open-orchestra
gh api repos/jterratsdev/open-orchestra/actions/runners --jq '.runners[] | {name, status, busy, labels: [.labels[].name]}'
npm run sonar:preflight:local
```

Expected result:

- `SONAR_RUNNER` is `self-hosted`.
- A runner is `online` with `self-hosted`, `sonar`, and `local-sonar` labels.
- Sonar authentication returns `{"valid":true}`.
- `npm run sonar:preflight:local` passes `auth`, `project`, `qualityGate`, and
  `issues`; `hotspots` may warn when the token lacks hotspot read access.
- Pull request runs on local SonarQube Community Edition may report
  `quality-gate-readable-default-branch` when branch-scoped quality gate status
  is unavailable but default-branch quality gate access is valid.

If the runner is online but jobs stay queued, verify the workflow labels match
the runner labels exactly. If Sonar preflight fails, fix the token/project
permissions before rerunning the workflow.

### Current Workflow Warning Triage

- Node 20 action warning: non-blocking today. Track upstream action upgrades and
  update actions when GitHub publishes Node 24-compatible major versions.
- GPG keyserver warning from the Sonar scanner action: non-blocking when scanner
  setup and analysis complete. Prefer self-hosted runner network allowlisting or
  a pinned scanner cache before treating it as release-blocking.
- Cache or tar warnings: non-blocking unless scanner dependencies fail to
  restore and analysis cannot start. Rebuild the runner cache or rerun the job
  before changing project code.

These warnings should be captured as CI annotations, but they do not override a
passing Sonar workflow unless the scanner, import, or final quality gate step
fails.

Sonar reads TypeScript through `tsconfig.sonar.json`, a standalone analyzer
config that mirrors the build compiler options but lowers only the analyzer
target to `ES2022`. Keep the main build target unchanged unless runtime support
changes; the Sonar-specific file exists because SonarQube 9.9 analyzers reject
newer TypeScript targets such as `ES2023`, including when they appear in an
extended config.

Do not commit local SonarQube data, tokens, database volumes, or exported source
snippets.

## Finding Triage

Sonar findings are not automatic fixes. Before remediation, classify each
finding as one of:

- `fix-required`: confirmed defect or maintainability issue that should be
  corrected now.
- `accepted-risk`: real finding accepted for a documented reason, owner, and
  review date.
- `false-positive`: analyzer cannot model the actual behavior.
- `tool-limitation`: edition, language, generated-code, or framework limitation.
- `deferred-debt`: valid issue intentionally scheduled for a later task.

ESLint suppressions and similar static-analysis exceptions must not be removed
blindly. Validate whether the suppression is still required, can be narrowed,
should be fixed, or must be accepted with linked rationale.

Recommended minimum quality gate for new code:

- 0 new blocker or critical issues.
- 0 new vulnerabilities.
- Security hotspots reviewed before release.
- Duplicated lines on new code below 3%.
- Maintainability rating A on new code.
- Reliability rating A on new code.
- Coverage reported from `coverage/lcov.info` for source files on every Sonar
  run.

The Orchestra Sonar import stores unresolved issues and Sonar security hotspots
in the uploaded `sonar-insights` artifact. When the gate fails only on
`new_security_hotspots_reviewed`, review the hotspot list in SonarQube, mark
each hotspot as safe, fixed, or accepted with rationale, then rerun the Sonar
workflow. Do not bypass this gate from code unless the Product Owner explicitly
accepts the security risk.
If the artifact includes `securityHotspotsUnavailableReason`, the configured
`SONAR_TOKEN` can read the quality gate and issues but cannot read the hotspots
API. Grant the token Browse plus security hotspot review/read permissions in
SonarQube before using the artifact as release evidence.

## Coverage Publishing

The Sonar workflow runs `npm run test:coverage` before analysis. That command
builds the TypeScript sources, runs the Node test suite through `c8`, and writes
LCOV to `coverage/lcov.info`.

`sonar.javascript.lcov.reportPaths=coverage/lcov.info` publishes the LCOV file
to Sonar. The report is generated from source maps, so coverage entries map back
to `src/*.ts` files instead of generated `dist/*.js` files.

Coverage is intentionally split by surface:

- Core TypeScript modules: included in LCOV.
- CLI entry points under `bin/`: included in LCOV when exercised by tests.
- VS Code extension runtime files under `extensions/`: included in LCOV when
  exercised by tests.
- Site and web console: excluded from LCOV until browser coverage is wired in;
  they require Playwright screenshots, traces, videos, or E2E reports as release
  evidence.
- Tests and E2E files: excluded from coverage accounting.
- Scripts: excluded from product coverage, but validated through CI commands
  and targeted script tests where they enforce delivery gates.

`coverage/` is ignored locally and should not be committed.

## New Code Baseline

The project uses `main` as the new-code reference branch through
`sonar.newCode.referenceBranch=main`. This aligns Sonar's changed-lines and
new-code behavior with the repository default branch and avoids falling back to a
legacy `master` reference.

The Sonar workflow also creates local `master` compatibility refs that point to
`origin/main` inside the temporary GitHub Actions checkout. This does not create
or push a real `master` branch; it only gives Sonar's SCM changed-lines analysis
a legacy fallback ref when the remote Sonar project still asks for `master`.

## Architecture Analysis

The intended architecture model is documented in
[`sonar-architecture-model.md`](sonar-architecture-model.md). Sonar's scanner can
discover JavaScript and TypeScript dependency graphs today, but Open Orchestra
does not yet commit Sonar-specific architecture directive files because the
portable architecture source of truth is still the repository documentation,
rules, and Orchestra review gates.

Until Sonar directives are adopted, architecture violations are enforced through:

- repo standards in `AGENTS.md` and neutral rule sources selected by Orchestra;
- architecture gate decisions and ADR-style records;
- code review against domain boundaries;
- tests that protect command contracts, workflow behavior, and generated
  guidance.

When Sonar directive support is configured for this project, it should use the
same domains from `sonar-architecture-model.md`; the two models must not diverge.

## Dependency Analysis

## Tool Boundaries

Use the tools together instead of treating one as a replacement for another:

- Sonar: bugs, code smells, maintainability, duplication, coverage, and security
  hotspots.
- Sonar dependency analysis/SCA: enabled when the connected Sonar plan supports
  it. Dependency manifests such as `package-lock.json` must remain visible to
  the scanner.
- Gitleaks: secrets in code, history, issues, prompts, lessons, evidence, logs,
  artifacts, and model output.
- `npm audit`: local and CI dependency vulnerability control for this package.
- jscpd: local duplicate-code detection and fast copy-paste feedback.
- `collection-standards`: semantic duplication of domain lists, command
  matrices, role/status lists, providers, fixtures, selectors, and validators.

If a Sonar run still reports `Dependency analysis skipped`, treat that as an
environment or plan-level SCA limitation, not as proof that dependency risk is
covered. Release evidence must then include `npm audit` and secret scanning
results, plus Dependabot or equivalent repository alerts when available.

When Sonar reports duplicated code that represents a repeated domain collection,
the remediation should load `collection-standards` and extract a typed source of
truth rather than only reshaping the copied block.

## SaaS Findings Architecture

Future SaaS integration should import or correlate Sonar findings as project
quality evidence without moving tenant source code through Open Orchestra unless
the tenant explicitly enables that mode.

Minimum SaaS controls:

- Bind each Sonar project to one tenant and one Open Orchestra project.
- Store Sonar tokens per tenant/project with least privilege and rotation
  metadata.
- Keep tenant findings isolated by tenant id, project id, provider, branch, and
  scan id.
- Persist finding provenance: detector, rule id, severity, component, branch,
  commit, quality gate status, imported timestamp, actor, and review state.
- Convert findings into Orchestra evidence, reviews, or GitHub issues with
  explicit owner and severity mapping.
- Apply retention policies per tenant and regulation profile.
- Never expose another tenant's findings, source paths, or scan metadata.
- Do not use Sonar as a prompt/log/secret scanner; route those surfaces through
  Gitleaks/redaction/quarantine policy before they reach agents or providers.
- Use `.gitleaks.toml` and `npm run secret-scan` for repository scanning.
  Runtime/SaaS secret scanning needs additional tenant-aware redaction,
  quarantine, provenance, and retention controls.

Suggested SaaS flow:

1. Tenant connects Sonar project with scoped token.
2. Open Orchestra stores provider binding and quality gate expectations.
3. CI or webhook publishes scan metadata to the SaaS.
4. SaaS imports quality gate status and selected findings.
5. Findings are deduplicated against existing Orchestra evidence/issues.
6. Security, Tech Lead, QA, or Release Manager reviews findings based on
   severity and release impact.
7. Approved mappings become task evidence, review blockers, or GitHub issues.

## Release Readiness

For this repository, Sonar should be treated as:

- Required when `SONAR_TOKEN` is configured in CI.
- Blocking at the remote quality gate level when `SONAR_QUALITY_GATE_WAIT=true`
  and the token has read permission for the Sonar project.
- Advisory for local/offline development.
- Blocker for release when the configured quality gate fails on new code.

For SaaS tenants, whether Sonar is required or advisory is a tenant policy
decision. Regulated tenants may require quality gate pass evidence before
release approval.
