diff --git a/.github/workflows/agent-label-sync.yml b/.github/workflows/agent-label-sync.yml deleted file mode 100644 index 290e09f0..00000000 --- a/.github/workflows/agent-label-sync.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Agent Label Sync - -on: - workflow_dispatch: - push: - paths: - - '.github/workflows/agent-label-sync.yml' - -permissions: - contents: read - issues: write - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Create or update agent labels - uses: actions/github-script@v7 - with: - script: | - const labels = [ - { name: "agent:ready", color: "0E8A16", description: "Task is ready to be picked up by an agent." }, - { name: "agent:in-progress", color: "1D76DB", description: "Agent is actively implementing this task." }, - { name: "agent:blocked", color: "B60205", description: "Agent cannot proceed without human decision/input." }, - { name: "risk:low", color: "0E8A16", description: "Low-risk change with small regression surface." }, - { name: "risk:medium", color: "FBCA04", description: "Medium-risk change requiring careful review." }, - { name: "risk:high", color: "D93F0B", description: "High-risk change; strict human gate required." }, - { name: "area:frontend", color: "5319E7", description: "Frontend/UI related work." }, - { name: "area:backend", color: "0052CC", description: "Backend/runtime logic related work." }, - { name: "area:infra", color: "0366D6", description: "Infrastructure/CI/tooling related work." }, - { name: "area:docs", color: "0075CA", description: "Documentation/process related work." }, - { name: "area:security", color: "B60205", description: "Security hardening or vulnerability-related work." }, - { name: "area:release", color: "C2E0C6", description: "Release/build/distribution related work." }, - { name: "kpi-digest", color: "D4C5F9", description: "Weekly KPI digest issue for tracking team metrics." }, - { name: "escaped-regression", color: "E99695", description: "Bug that escaped to production/main post-merge." } - ]; - - for (const label of labels) { - try { - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - new_name: label.name, - color: label.color, - description: label.description - }); - } catch (error) { - if (error.status === 404) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description - }); - } else { - throw error; - } - } - } diff --git a/.github/workflows/agent-task-queue.yml b/.github/workflows/agent-task-queue.yml deleted file mode 100644 index 1c8b5227..00000000 --- a/.github/workflows/agent-task-queue.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: Agent Task Queue - -on: - issues: - types: [opened, labeled] - -permissions: - contents: read - issues: write - -jobs: - queue: - if: >- - ${{ github.event.issue.pull_request == null && ((github.event.action == 'labeled' && github.event.label.name == 'agent:ready') || (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'agent:ready'))) }} - runs-on: ubuntu-latest - env: - VERIFY_COMMAND: bash scripts/verify - steps: - - name: Build agent task packet and notify Copilot - uses: actions/github-script@v7 - with: - script: | - const issue = context.payload.issue; - const owner = context.repo.owner; - const repo = context.repo.repo; - const issue_number = issue.number; - - const currentLabels = (issue.labels || []).map(l => typeof l === "string" ? l : l.name); - const hadReady = currentLabels.includes("agent:ready"); - const hadInProgress = currentLabels.includes("agent:in-progress"); - - if (!hadReady && context.payload.action === "opened") { - core.info("Issue opened without agent:ready; skipping queue."); - return; - } - - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number, - per_page: 100, - }); - - const existingContract = comments.find(c => - c.body?.includes("### Execution Contract") && - c.body?.includes(`- #${issue_number}:`) - ); - - let addedInProgress = false; - let removedReady = false; - - try { - if (!hadInProgress) { - await github.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: ["agent:in-progress"], - }); - addedInProgress = true; - } - - if (hadReady) { - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number, - name: "agent:ready", - }); - removedReady = true; - } catch (error) { - if (error.status === 404) { - core.info("agent:ready was already removed."); - } else { - throw error; - } - } - } - - if (existingContract) { - core.info("Execution contract already exists; transition applied without duplicate enqueue."); - return; - } - - const lines = [ - "@copilot Please implement this task using repository guardrails.", - "", - "### Execution Contract", - "1. Keep the change minimal and in scope.", - "2. Run deterministic verification before requesting review.", - `3. Required verification command: \`${process.env.VERIFY_COMMAND}\`.`, - "4. Include PR sections: Summary, Risk, Evidence, Rollback, Scope Guard.", - "5. Do not merge; maintainers perform final human review.", - "", - "### Source Issue", - `- #${issue_number}: ${issue.title}`, - ]; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: lines.join("\n"), - }); - } catch (error) { - core.warning(`Queue transition failed: ${error.message}`); - - if (addedInProgress && !hadInProgress) { - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number, - name: "agent:in-progress", - }); - } catch (rollbackError) { - core.warning(`Rollback remove agent:in-progress failed: ${rollbackError.message}`); - } - } - - if (removedReady && hadReady) { - try { - await github.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: ["agent:ready"], - }); - } catch (rollbackError) { - core.warning(`Rollback add agent:ready failed: ${rollbackError.message}`); - } - } - - throw error; - } diff --git a/.github/workflows/applitools-visual.yml b/.github/workflows/applitools-visual.yml new file mode 100644 index 00000000..befbf587 --- /dev/null +++ b/.github/workflows/applitools-visual.yml @@ -0,0 +1,31 @@ +name: Applitools Visual + +on: + workflow_dispatch: + pull_request: + branches: [main, master] + push: + branches: [main, master] + +permissions: + contents: read + +jobs: + applitools-visual: + name: Applitools Visual + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/webcoder_ui + env: + APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: frontend/webcoder_ui/package-lock.json + - run: npm ci + - run: npx --yes playwright install --with-deps chromium + - run: npm run visual:applitools diff --git a/.github/workflows/chromatic-playwright.yml b/.github/workflows/chromatic-playwright.yml new file mode 100644 index 00000000..b70bf948 --- /dev/null +++ b/.github/workflows/chromatic-playwright.yml @@ -0,0 +1,32 @@ +name: Chromatic Playwright + +on: + workflow_dispatch: + pull_request: + branches: [main, master] + push: + branches: [main, master] + +permissions: + contents: read + +jobs: + chromatic-playwright: + name: Chromatic Playwright + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/webcoder_ui + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: frontend/webcoder_ui/package-lock.json + - run: npm ci + - run: npx --yes playwright install --with-deps chromium + - run: npm run visual:chromatic:archive + - run: npx --yes chromatic --playwright --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} + env: + CHROMATIC_ARCHIVE_LOCATION: frontend/webcoder_ui/test-results diff --git a/.github/workflows/codacy-zero.yml b/.github/workflows/codacy-zero.yml deleted file mode 100644 index 3184250a..00000000 --- a/.github/workflows/codacy-zero.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Codacy Zero - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - codacy-zero: - name: Codacy Zero - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Assert Codacy zero-open gate - env: - CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} - run: | - python3 scripts/quality/check_codacy_zero.py \ - --owner "${GITHUB_REPOSITORY_OWNER}" \ - --repo "${GITHUB_REPOSITORY#*/}" \ - --out-json "codacy-zero/codacy.json" \ - --out-md "codacy-zero/codacy.md" - - name: Upload Codacy artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: codacy-zero - path: codacy-zero diff --git a/.github/workflows/codecov-analytics.yml b/.github/workflows/codecov-analytics.yml index 676d3961..20572f73 100644 --- a/.github/workflows/codecov-analytics.yml +++ b/.github/workflows/codecov-analytics.yml @@ -1,5 +1,9 @@ name: Codecov Analytics +permissions: + contents: read + id-token: write + on: push: branches: [main, master] @@ -7,51 +11,17 @@ on: branches: [main, master] workflow_dispatch: -permissions: - contents: read - jobs: - codecov-analytics: - name: Codecov Analytics - runs-on: ubuntu-latest - env: + shared-codecov-analytics: + permissions: + contents: read + id-token: write + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-codecov-analytics.yml@2be048255a27d55eb7be18b82091b57f29d34ee6 + with: + repo_slug: ${{ github.repository }} + event_name: ${{ github.event_name }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} + platform_repository: Prekzursil/quality-zero-platform + platform_ref: 2be048255a27d55eb7be18b82091b57f29d34ee6 + secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} - CODACY_ORGANIZATION_PROVIDER: gh - CODACY_USERNAME: Prekzursil - CODACY_PROJECT_NAME: ${{ github.event.repository.name }} - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - uses: actions/setup-node@v6 - with: - node-version: '20' - - - name: Backend coverage - continue-on-error: true - run: | - mkdir -p coverage - python -m pip install --upgrade pip - python -m pip install -r backend/requirements.txt pytest pytest-cov - python -m pytest backend --cov=backend --cov-report=xml:backend/coverage.xml - - name: Frontend coverage - continue-on-error: true - run: | - npm --prefix frontend/webcoder_ui ci - npm --prefix frontend/webcoder_ui test -- --coverage --watch=false - - name: Upload coverage to Codecov - if: ${{ always() }} - uses: codecov/codecov-action@v5 - with: - files: backend/coverage.xml,frontend/webcoder_ui/coverage/lcov.info - flags: backend,frontend - fail_ci_if_error: true - verbose: true - - name: Upload coverage to Codacy - if: ${{ always() }} - uses: codacy/codacy-coverage-reporter-action@v1 - with: - api-token: ${{ env.CODACY_API_TOKEN }} - coverage-reports: backend/coverage.xml,frontend/webcoder_ui/coverage/lcov.info diff --git a/.github/workflows/coverage-100.yml b/.github/workflows/coverage-100.yml deleted file mode 100644 index d2c736eb..00000000 --- a/.github/workflows/coverage-100.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Coverage 100 - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - coverage-100: - name: Coverage 100 Gate - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - uses: actions/setup-node@v6 - with: - node-version: '20' - - - name: Backend coverage - run: | - mkdir -p coverage - python -m pip install --upgrade pip - python -m pip install -r backend/requirements.txt pytest pytest-cov - python -m pytest backend --cov=backend --cov-report=xml:backend/coverage.xml - - name: Frontend coverage - run: | - npm --prefix frontend/webcoder_ui ci - npm --prefix frontend/webcoder_ui test -- --coverage --watch=false - - - name: Enforce 100% coverage - run: | - python3 scripts/quality/assert_coverage_100.py \ - --xml "backend=backend/coverage.xml" \ - --lcov "frontend=frontend/webcoder_ui/coverage/lcov.info" \ - --out-json "coverage-100/coverage.json" \ - --out-md "coverage-100/coverage.md" - - name: Upload coverage artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-100 - path: | - coverage - **/coverage - **/TestResults diff --git a/.github/workflows/deepscan-zero.yml b/.github/workflows/deepscan-zero.yml deleted file mode 100644 index 824afdb7..00000000 --- a/.github/workflows/deepscan-zero.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: DeepScan Zero - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - checks: read - -jobs: - deepscan-zero: - name: DeepScan Zero - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v6 - - name: Assert DeepScan vendor check is green - run: | - python3 scripts/quality/check_required_checks.py \ - --repo "${GITHUB_REPOSITORY}" \ - --sha "${GITHUB_SHA}" \ - --required-context "DeepScan" \ - --timeout-seconds 1200 \ - --poll-seconds 20 \ - --out-json "deepscan-zero/deepscan.json" \ - --out-md "deepscan-zero/deepscan.md" - - name: Upload DeepScan artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: deepscan-zero - path: deepscan-zero diff --git a/.github/workflows/kpi-weekly-digest.yml b/.github/workflows/kpi-weekly-digest.yml deleted file mode 100644 index 4f8b8697..00000000 --- a/.github/workflows/kpi-weekly-digest.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: KPI Weekly Digest - -on: - workflow_dispatch: - schedule: - - cron: '15 6 * * 1' - -permissions: - contents: read - issues: write - pull-requests: read - -jobs: - digest: - runs-on: ubuntu-latest - steps: - - name: Publish weekly KPI digest issue - uses: actions/github-script@v7 - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - - const now = new Date(); - const since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - const sinceIso = since.toISOString(); - - const requiredLabels = [ - { name: "area:infra", color: "0366D6", description: "Infrastructure/CI/tooling related work." }, - { name: "risk:low", color: "0E8A16", description: "Low-risk change with small regression surface." }, - { name: "kpi-digest", color: "D4C5F9", description: "Weekly KPI digest issue for tracking team metrics." }, - ]; - - for (const label of requiredLabels) { - try { - await github.rest.issues.updateLabel({ - owner, - repo, - name: label.name, - new_name: label.name, - color: label.color, - description: label.description, - }); - } catch (error) { - if (error.status === 404) { - await github.rest.issues.createLabel({ - owner, - repo, - name: label.name, - color: label.color, - description: label.description, - }); - } else { - throw error; - } - } - } - - const pulls = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "all", - sort: "updated", - direction: "desc", - per_page: 100, - }); - - const mergedLast7d = pulls.filter(pr => pr.merged_at && new Date(pr.merged_at) >= since).length; - const openedLast7d = pulls.filter(pr => new Date(pr.created_at) >= since).length; - - const openIssues = await github.paginate(github.rest.issues.listForRepo, { - owner, - repo, - state: "open", - per_page: 100, - }); - - const openAgentIssues = openIssues.filter( - i => - !i.pull_request && - (i.labels || []).some(l => ["agent:ready", "agent:in-progress", "agent:blocked"].includes(l.name)) - ).length; - - const title = `Weekly KPI Digest - ${now.toISOString().slice(0, 10)}`; - const body = [ - "## Summary", - `- Window start: ${sinceIso}`, - `- Window end: ${now.toISOString()}`, - "", - "## Automated Snapshot", - `- PRs opened (7d): ${openedLast7d}`, - `- PRs merged (7d): ${mergedLast7d}`, - `- Open agent-tracked issues: ${openAgentIssues}`, - "", - "## KPI Fields (fill/verify)", - "- Intake-to-PR lead time:", - "- PR cycle time:", - "- Queue failure rate:", - "- Agent rework rate:", - "- Evidence completeness rate:", - "- Regression incident count:", - "", - "## Notes", - "- Add blockers, regressions, and remediation actions.", - ].join("\n"); - - await github.rest.issues.create({ - owner, - repo, - title, - body, - labels: ["area:infra", "risk:low", "kpi-digest"], - }); diff --git a/.github/workflows/quality-zero-backlog.yml b/.github/workflows/quality-zero-backlog.yml new file mode 100644 index 00000000..5b467d16 --- /dev/null +++ b/.github/workflows/quality-zero-backlog.yml @@ -0,0 +1,19 @@ +name: Quality Zero Backlog + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + inputs: + tool: + description: Backlog tool lane + required: true + default: coverage + +jobs: + backlog-sweep: + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-backlog-sweep.yml@main + with: + repo_slug: ${{ github.repository }} + tool: ${{ inputs.tool || 'coverage' }} + secrets: inherit diff --git a/.github/workflows/quality-zero-gate.yml b/.github/workflows/quality-zero-gate.yml index 35256f9e..49de3831 100644 --- a/.github/workflows/quality-zero-gate.yml +++ b/.github/workflows/quality-zero-gate.yml @@ -1,5 +1,8 @@ name: Quality Zero Gate +permissions: + contents: read + on: push: branches: [main, master] @@ -7,75 +10,14 @@ on: branches: [main, master] workflow_dispatch: -permissions: - contents: read - jobs: - secrets-preflight: - name: Quality Secrets Preflight - runs-on: ubuntu-latest - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} - DEEPSCAN_POLICY_MODE: ${{ vars.DEEPSCAN_POLICY_MODE }} - DEEPSCAN_OPEN_ISSUES_URL: ${{ vars.DEEPSCAN_OPEN_ISSUES_URL }} - DEEPSCAN_API_TOKEN: ${{ secrets.DEEPSCAN_API_TOKEN }} - steps: - - uses: actions/checkout@v6 - - name: Run quality secrets preflight - run: | - python3 scripts/quality/check_quality_secrets.py \ - --out-json quality-secrets/secrets.json \ - --out-md quality-secrets/secrets.md - - name: Upload secrets preflight artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: quality-secrets - path: quality-secrets - - quality-zero-gate: - name: Quality Zero Gate - if: always() - runs-on: ubuntu-latest - needs: - - secrets-preflight - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v6 - - name: Assert secrets preflight succeeded - run: | - if [ "${{ needs.secrets-preflight.result }}" != "success" ]; then - echo "Quality Secrets Preflight failed or was not successful." >&2 - exit 1 - fi - - name: Assert required quality contexts are green - run: | - python3 scripts/quality/check_required_checks.py \ - --repo "${GITHUB_REPOSITORY}" \ - --sha "${GITHUB_SHA}" \ - --required-context "Coverage 100 Gate" \ - --required-context "Codecov Analytics" \ - --required-context "Sonar Zero" \ - --required-context "Codacy Zero" \ - --required-context "Semgrep Zero" \ - --required-context "Sentry Zero" \ - --required-context "DeepScan Zero" \ - --required-context "SonarCloud Code Analysis" \ - --required-context "Codacy Static Code Analysis" \ - --required-context "DeepScan" \ - --timeout-seconds 1500 \ - --poll-seconds 20 \ - --out-json quality-zero-gate/required-checks.json \ - --out-md quality-zero-gate/required-checks.md - - name: Upload aggregate artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: quality-zero-gate - path: quality-zero-gate + aggregate-gate: + permissions: + contents: read + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-quality-zero-gate.yml@2be048255a27d55eb7be18b82091b57f29d34ee6 + with: + repo_slug: ${{ github.repository }} + event_name: ${{ github.event_name }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} + platform_repository: Prekzursil/quality-zero-platform + platform_ref: 2be048255a27d55eb7be18b82091b57f29d34ee6 diff --git a/.github/workflows/quality-zero-platform.yml b/.github/workflows/quality-zero-platform.yml new file mode 100644 index 00000000..c6059753 --- /dev/null +++ b/.github/workflows/quality-zero-platform.yml @@ -0,0 +1,31 @@ +name: Quality Zero Platform + +permissions: + contents: read + id-token: write + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +jobs: + shared-scanner-matrix: + permissions: + contents: read + id-token: write + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-scanner-matrix.yml@2be048255a27d55eb7be18b82091b57f29d34ee6 + with: + repo_slug: ${{ github.repository }} + event_name: ${{ github.event_name }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} + platform_repository: Prekzursil/quality-zero-platform + platform_ref: 2be048255a27d55eb7be18b82091b57f29d34ee6 + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + DEEPSCAN_API_TOKEN: ${{ secrets.DEEPSCAN_API_TOKEN }} diff --git a/.github/workflows/quality-zero-remediation.yml b/.github/workflows/quality-zero-remediation.yml new file mode 100644 index 00000000..d0aae017 --- /dev/null +++ b/.github/workflows/quality-zero-remediation.yml @@ -0,0 +1,22 @@ +name: Quality Zero Remediation + +on: + workflow_run: + workflows: ["Quality Zero Gate"] + types: [completed] + workflow_dispatch: + inputs: + failure_context: + description: Failed context to remediate + required: false + default: Quality Zero Gate + +jobs: + remediate: + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-remediation-loop.yml@main + with: + repo_slug: ${{ github.repository }} + failure_context: ${{ inputs.failure_context || 'Quality Zero Gate' }} + sha: ${{ github.event.workflow_run.head_sha || github.sha }} + secrets: inherit diff --git a/.github/workflows/semgrep-zero.yml b/.github/workflows/semgrep-zero.yml deleted file mode 100644 index ba5cf7ed..00000000 --- a/.github/workflows/semgrep-zero.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: Semgrep Zero - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - semgrep-zero: - name: Semgrep Zero - runs-on: ubuntu-latest - env: - SEMGREP_SEND_METRICS: "off" - SEMGREP_SRC_ROOT: . - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 - with: - python-version: "3.12" - - name: Install Semgrep - run: python -m pip install --upgrade pip semgrep - - name: Select Semgrep rules - shell: bash - run: | - set -euo pipefail - mkdir -p .tmp - cat > .tmp/semgrep-configs.txt <<'CONFIGS' - p/default - p/secrets - p/github-actions - CONFIGS - if find . -type f \( -name '*.py' -o -name 'pyproject.toml' \) -print -quit | grep -q .; then - echo p/python >> .tmp/semgrep-configs.txt - fi - if find . -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' -o -name 'package.json' \) -print -quit | grep -q .; then - echo p/javascript >> .tmp/semgrep-configs.txt - fi - # p/default already covers the currently available baseline rules for C and C++. - # A dedicated p/cpp pack is not available in the current Semgrep registry. - if find . -type f \( -name '*.cs' -o -name '*.csproj' -o -name '*.sln' \) -print -quit | grep -q .; then - echo p/csharp >> .tmp/semgrep-configs.txt - fi - - name: Clear proxy environment for Semgrep - shell: bash - run: | - set -euo pipefail - for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy; do - echo "${key}=" >> "$GITHUB_ENV" - done - - name: Run Semgrep JSON gate - shell: bash - run: | - set -euo pipefail - unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy - mkdir -p semgrep-zero - mapfile -t configs < .tmp/semgrep-configs.txt - args=() - for cfg in "${configs[@]}"; do - [ -n "$cfg" ] || continue - args+=(--config "$cfg") - done - semgrep scan \ - --error \ - --metrics=off \ - --json \ - --output semgrep-zero/semgrep.json \ - --exclude .git \ - --exclude .venv \ - --exclude venv \ - --exclude node_modules \ - --exclude dist \ - --exclude build \ - --exclude coverage \ - --exclude coverage-100 \ - --exclude third_party \ - --exclude vendor \ - "${args[@]}" \ - "${SEMGREP_SRC_ROOT}" - - name: Render SARIF artifact - if: always() - shell: bash - run: | - set -euo pipefail - unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy - mkdir -p semgrep-zero - mapfile -t configs < .tmp/semgrep-configs.txt - args=() - for cfg in "${configs[@]}"; do - [ -n "$cfg" ] || continue - args+=(--config "$cfg") - done - semgrep scan \ - --metrics=off \ - --sarif \ - --output semgrep-zero/semgrep.sarif \ - --exclude .git \ - --exclude .venv \ - --exclude venv \ - --exclude node_modules \ - --exclude dist \ - --exclude build \ - --exclude coverage \ - --exclude coverage-100 \ - --exclude third_party \ - --exclude vendor \ - "${args[@]}" \ - "${SEMGREP_SRC_ROOT}" - - name: Upload Semgrep artifacts - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: semgrep-zero - path: semgrep-zero diff --git a/.github/workflows/sentry-zero.yml b/.github/workflows/sentry-zero.yml deleted file mode 100644 index d9aeea83..00000000 --- a/.github/workflows/sentry-zero.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Sentry Zero - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - sentry-zero: - name: Sentry Zero - runs-on: ubuntu-latest - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} - steps: - - uses: actions/checkout@v6 - - name: Assert Sentry unresolved issues are zero - run: | - python3 scripts/quality/check_sentry_zero.py \ - --project "${SENTRY_PROJECT}" \ - --out-json "sentry-zero/sentry.json" \ - --out-md "sentry-zero/sentry.md" - - name: Upload Sentry artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: sentry-zero - path: sentry-zero diff --git a/.github/workflows/sonar-zero.yml b/.github/workflows/sonar-zero.yml deleted file mode 100644 index 0e7bb386..00000000 --- a/.github/workflows/sonar-zero.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Sonar Zero - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - sonar-zero: - name: Sonar Zero - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Run Sonar scan - uses: SonarSource/sonarqube-scan-action@v6 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Assert Sonar zero-open gate - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - python3 scripts/quality/check_sonar_zero.py \ - --project-key "Prekzursil_WebCoder" \ - --out-json "sonar-zero/sonar.json" \ - --out-md "sonar-zero/sonar.md" - - name: Upload Sonar artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: sonar-zero - path: sonar-zero diff --git a/AGENTS.md b/AGENTS.md index 7de2d4ab..6d60cffc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,8 @@ ## Operating Model -This repository follows an evidence-first, zero-external-API-cost workflow. -Use GitHub Copilot coding agent and Codex app/IDE/CLI for implementation and review, then verify with deterministic commands before merge. - -## Risk Policy - -- Default merge policy: human-reviewed only. -- Use explicit risk labels: `risk:low`, `risk:medium`, `risk:high`. -- High-risk changes require rollback notes in the PR. +This repository uses shared `quality-zero-platform` wrapper workflows for strict-zero quality automation. +Keep changes evidence-backed, small, and task-focused. ## Canonical Verification Command @@ -21,16 +15,6 @@ bash scripts/verify ## Scope Guardrails -- Keep changes small and task-focused. - Do not commit secrets or local runtime artifacts. - Prefer tests/docs updates together with behavior changes. - -## Agent Queue Contract - -- Intake issues via `.github/ISSUE_TEMPLATE/agent_task.yml`. -- Queue work by adding `agent:ready` label. -- Queue workflow will post a task packet and notify `@copilot`. - -## Queue Trigger Warning - -Applying label `agent:ready` triggers the queue workflow immediately. +- Treat missing external statuses as policy drift before code changes. diff --git a/frontend/webcoder_ui/package.json b/frontend/webcoder_ui/package.json index 29e44e38..b18045bb 100644 --- a/frontend/webcoder_ui/package.json +++ b/frontend/webcoder_ui/package.json @@ -29,7 +29,10 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "visual:test": "npx --yes --package=@playwright/test --package=playwright playwright test --config=playwright.config.cjs tests/visual/public-routes.playwright.spec.mjs", + "visual:chromatic:archive": "npx --yes --package=@chromatic-com/playwright --package=@playwright/test --package=playwright playwright test --config=playwright.config.cjs tests/visual/public-routes.chromatic.spec.mjs", + "visual:applitools": "npx --yes --package=@playwright/test --package=playwright --package=@applitools/eyes-playwright playwright test --config=playwright.config.cjs tests/visual/public-routes.applitools.spec.mjs" }, "eslintConfig": { "extends": [ diff --git a/frontend/webcoder_ui/playwright.config.cjs b/frontend/webcoder_ui/playwright.config.cjs new file mode 100644 index 00000000..7f70a55b --- /dev/null +++ b/frontend/webcoder_ui/playwright.config.cjs @@ -0,0 +1,30 @@ +const { defineConfig } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests/visual', + timeout: 30_000, + expect: { + timeout: 10_000, + }, + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000', + viewport: { width: 1440, height: 900 }, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + webServer: { + command: 'npm start', + url: process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { + ...process.env, + BROWSER: 'none', + CI: process.env.CI || 'true', + HOST: '127.0.0.1', + PORT: '3000', + }, + }, +}); diff --git a/frontend/webcoder_ui/src/visual/publicVisualRoutes.test.ts b/frontend/webcoder_ui/src/visual/publicVisualRoutes.test.ts new file mode 100644 index 00000000..5d5d4e22 --- /dev/null +++ b/frontend/webcoder_ui/src/visual/publicVisualRoutes.test.ts @@ -0,0 +1,31 @@ +const { publicVisualRoutes } = require('../../visual/publicVisualRoutes.cjs'); + +describe('publicVisualRoutes', () => { + it('keeps the visual seam limited to public non-auth routes', () => { + expect(publicVisualRoutes).toEqual([ + { + name: 'home', + path: '/', + heading: 'Welcome to WebCoder', + primaryAction: 'View Problems', + }, + { + name: 'login', + path: '/login', + heading: 'Login', + primaryAction: 'Login', + }, + { + name: 'register', + path: '/register', + heading: 'Register', + primaryAction: 'Register', + }, + ]); + }); + + it('uses unique route names and paths', () => { + expect(new Set(publicVisualRoutes.map((route) => route.name)).size).toBe(publicVisualRoutes.length); + expect(new Set(publicVisualRoutes.map((route) => route.path)).size).toBe(publicVisualRoutes.length); + }); +}); diff --git a/frontend/webcoder_ui/tests/visual/helpers.mjs b/frontend/webcoder_ui/tests/visual/helpers.mjs new file mode 100644 index 00000000..4fc2475b --- /dev/null +++ b/frontend/webcoder_ui/tests/visual/helpers.mjs @@ -0,0 +1,11 @@ +import routesModule from '../../visual/publicVisualRoutes.cjs'; + +const { publicVisualRoutes } = routesModule; + +export { publicVisualRoutes }; + +export async function openPublicRoute(page, route) { + await page.goto(route.path, { waitUntil: 'networkidle' }); + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.locator('main').waitFor({ state: 'visible' }); +} diff --git a/frontend/webcoder_ui/tests/visual/public-routes.applitools.spec.mjs b/frontend/webcoder_ui/tests/visual/public-routes.applitools.spec.mjs new file mode 100644 index 00000000..84007b6d --- /dev/null +++ b/frontend/webcoder_ui/tests/visual/public-routes.applitools.spec.mjs @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; +import { BatchInfo, Eyes, Target } from '@applitools/eyes-playwright'; +import { publicVisualRoutes, openPublicRoute } from './helpers.mjs'; + +const batch = new BatchInfo('WebCoder public routes'); + +for (const route of publicVisualRoutes) { + test(`${route.name} applitools snapshot`, async ({ page }) => { + const eyes = new Eyes(); + eyes.setBatch(batch); + + await eyes.open(page, 'WebCoder', `public route: ${route.name}`, { width: 1440, height: 900 }); + await openPublicRoute(page, route); + + await expect(page.getByRole('heading', { name: route.heading, exact: true })).toBeVisible(); + await eyes.check(route.name, Target.window().fully()); + await eyes.close(false); + }); +} diff --git a/frontend/webcoder_ui/tests/visual/public-routes.chromatic.spec.mjs b/frontend/webcoder_ui/tests/visual/public-routes.chromatic.spec.mjs new file mode 100644 index 00000000..f86c3c38 --- /dev/null +++ b/frontend/webcoder_ui/tests/visual/public-routes.chromatic.spec.mjs @@ -0,0 +1,11 @@ +import { test, expect } from '@chromatic-com/playwright'; +import { publicVisualRoutes, openPublicRoute } from './helpers.mjs'; + +for (const route of publicVisualRoutes) { + test(`${route.name} chromatic snapshot`, async ({ page }) => { + await openPublicRoute(page, route); + + await expect(page.getByRole('heading', { name: route.heading, exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: route.primaryAction, exact: true })).toBeVisible(); + }); +} diff --git a/frontend/webcoder_ui/tests/visual/public-routes.playwright.spec.mjs b/frontend/webcoder_ui/tests/visual/public-routes.playwright.spec.mjs new file mode 100644 index 00000000..4bd81764 --- /dev/null +++ b/frontend/webcoder_ui/tests/visual/public-routes.playwright.spec.mjs @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test'; +import { publicVisualRoutes, openPublicRoute } from './helpers.mjs'; + +for (const route of publicVisualRoutes) { + test(`${route.name} route renders without authentication`, async ({ page }) => { + await openPublicRoute(page, route); + + await expect(page.getByRole('heading', { name: route.heading, exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: route.primaryAction, exact: true })).toBeVisible(); + await expect(page).toHaveURL(new RegExp(`${route.path === '/' ? '/?$' : `${route.path}/?$`}`)); + }); +} diff --git a/frontend/webcoder_ui/visual/publicVisualRoutes.cjs b/frontend/webcoder_ui/visual/publicVisualRoutes.cjs new file mode 100644 index 00000000..2b014781 --- /dev/null +++ b/frontend/webcoder_ui/visual/publicVisualRoutes.cjs @@ -0,0 +1,22 @@ +const publicVisualRoutes = Object.freeze([ + Object.freeze({ + name: 'home', + path: '/', + heading: 'Welcome to WebCoder', + primaryAction: 'View Problems', + }), + Object.freeze({ + name: 'login', + path: '/login', + heading: 'Login', + primaryAction: 'Login', + }), + Object.freeze({ + name: 'register', + path: '/register', + heading: 'Register', + primaryAction: 'Register', + }), +]); + +module.exports = { publicVisualRoutes };