diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..210ac0b21 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,432 @@ +# Build workflow โ€” the fast "build" group as steps in a single job: version validation, install, localization +# check, external-skills check, lint, prettier, compile (tsc) and packaging (VSIX). +# +# Design: +# - All these checks are fast, so they live as steps in ONE job (no per-job runner spin-up / dependency +# reinstall). The SLOW tests run in their own workflows so they execute IN PARALLEL with this job on +# separate runners: unit + integration in test.yml, Playwright in e2e.yml. +# - "Run everything, then fail if anything failed": after `npm ci` succeeds, every analyzer step carries +# `if: ${{ !cancelled() && steps.install.outcome == 'success' }}`, so a failing analyzer does NOT stop the +# others โ€” one run surfaces ALL problems (e.g. both a lint and a localization failure). Any failed step +# still marks the whole job red. (`npm ci` is the one hard prerequisite: if it fails, the analyzers are +# pointless and are skipped.) + +name: Build + +permissions: + contents: read + pull-requests: write + # Needed by the summary step's gh api calls (list this run's artifacts). + actions: read + +on: + workflow_dispatch: + + push: + branches: + - main + - rel/* + - dev/** + + pull_request: + branches: + - main + - rel/* + - dev/** + +# Workflow-level concurrency dedupes runs for the SAME branch across DIFFERENT commits (e.g. rapid pushes +# during a rebase). For pull_request events the group is scoped by PR number + head repo so two forks pushing +# branches with the same name don't cancel each other. +concurrency: + group: >- + build-${{ github.workflow }}-${{ + github.event_name == 'pull_request' && + format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || + github.ref_name }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + # A push to a branch with an open PR fires both `push` and `pull_request` events. Cancelling one of them + # would leave a failed/cancelled required check on the PR head and block merge, so instead we SKIP the + # `pull_request` run for same-repo PRs whose head branch is already covered by this workflow's + # `push.branches` filters (only those branches get a `push` run in this repo). Fork PRs and same-repo PRs + # from other head branches (e.g. feature/*) still run on `pull_request`. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository || + (github.head_ref != 'main' && + !startsWith(github.head_ref, 'rel/') && + !startsWith(github.head_ref, 'dev/')) + + steps: + - uses: actions/checkout@v6 + + - name: ๐Ÿ“ฆ Using Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + # Hard prerequisite: if dependencies don't install, every analyzer below is skipped (they all gate on + # `steps.install.outcome == 'success'`). + - name: โฌ‡๏ธ Install Dependencies + id: install + run: npm ci + + - name: โš–๏ธ Validate Version / Preview Alignment + id: validate-version + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + shell: pwsh + run: | + $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json + $version = $packageJson.version + $preview = [bool]$packageJson.preview + + Write-Output "๐Ÿ“ฆ Package version: $version" + Write-Output "๐Ÿท๏ธ Preview flag: $preview" + + # Validate semantic versioning format + if ($version -notmatch '^(\d+)\.(\d+)\.(\d+)$') { + Write-Error "โŒ Invalid semantic version format: $version" + exit 1 + } + + $minor = [int]$Matches[2] + $minorEven = ($minor % 2) -eq 0 + + # Check version/preview alignment + # Convention: odd minor versions = preview, even minor versions = stable + if ($preview -and $minorEven) { + Write-Warning ("โš ๏ธ Version/preview misalignment: preview=$preview " + + "but minor version $minor is even (should be odd for preview)") + Write-Warning ("Convention: Odd minor versions (e.g., 1.1.x, 1.3.x) should be preview, " + + "even (e.g., 1.0.x, 1.2.x) should be stable") + } elseif (-not $preview -and -not $minorEven) { + Write-Warning ("โš ๏ธ Version/preview misalignment: preview=$preview " + + "but minor version $minor is odd (should be even for stable)") + Write-Warning ("Convention: Odd minor versions (e.g., 1.1.x, 1.3.x) should be preview, " + + "even (e.g., 1.0.x, 1.2.x) should be stable") + } else { + Write-Output "โœ… Version/preview alignment is correct" + } + + - name: ๐ŸŒ Localize + id: l10n + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run l10n:check + + - name: ๐Ÿ” Verify External Skills + id: skills + # Warning-only: this step never fails the workflow. It emits a ::warning:: when the committed skills/ + # drift from the pinned commit, but committed-skill drift should not block a PR. + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + shell: bash + run: | + cp -r skills/ skills-backup/ + npm run fetch-skill + # Use --strip-trailing-cr -B -b to ignore line ending, blank line, and whitespace differences + if ! diff -rq --strip-trailing-cr -B -b skills/ skills-backup/ > /dev/null 2>&1; then + echo "::warning::Committed skills/ does not match the pinned commit in package.json. If you have local skill changes, submit them to the upstream skill repository first, then update the pinned commit in package.json and run 'npm run fetch-skill' to pull the latest version." + diff -r --strip-trailing-cr -B -b skills/ skills-backup/ || true + else + echo "โœ… External skills are up to date." + fi + rm -rf skills/ + mv skills-backup/ skills/ + + - name: ๐Ÿงน Lint + id: lint + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run lint + + - name: โœจ Prettier + id: prettier + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run prettier + + - name: ๐Ÿ”จ Compile + id: compile + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run build + + - name: ๐Ÿ“ฆ Package + id: package + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run package + + - name: ๐Ÿ” Verify VSIX File + id: verify-vsix + # Only meaningful when packaging produced a VSIX. + if: ${{ !cancelled() && steps.package.outcome == 'success' }} + shell: pwsh + run: | + # Find VSIX file + $vsixFiles = Get-ChildItem -Path . -Filter *.vsix -File + + if ($vsixFiles.Count -eq 0) { + Write-Error "โŒ No VSIX file found" + exit 1 + } elseif ($vsixFiles.Count -gt 1) { + Write-Error "โŒ Multiple VSIX files found: $($vsixFiles.Name -join ', ')" + exit 1 + } + + $vsixFile = $vsixFiles[0] + $vsixFileName = $vsixFile.Name + $vsixFileSize = [math]::Round($vsixFile.Length / 1MB, 2) + + Write-Output "โœ… Found VSIX: $vsixFileName (${vsixFileSize} MB)" + + # Verify filename matches expected pattern + $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json + $expectedName = "$($packageJson.name)-$($packageJson.version).vsix" + + if ($vsixFileName -ne $expectedName) { + Write-Error "โŒ VSIX filename mismatch: expected '$expectedName', got '$vsixFileName'" + exit 1 + } + + Write-Output "โœ… VSIX filename is correct: $vsixFileName" + + # Compute a short commit SHA (7 chars) and a filesystem-safe branch slug. + # Use head_ref for pull_request events, otherwise ref_name. + $rawRef = if ("${{ github.event_name }}" -eq "pull_request") { + "${{ github.head_ref }}" + } else { + "${{ github.ref_name }}" + } + $branchSlug = ($rawRef -replace '[^A-Za-z0-9._-]', '-').Trim('-') + $shortSha = "${{ github.sha }}".Substring(0, 7) + + # Artifact name convention: + # - main / rel/* : -- + # - everything else: -- + $isRelease = ($rawRef -eq 'main') -or ($rawRef -like 'rel/*') + if ($isRelease) { + $artifactName = "$($packageJson.name)-$($packageJson.version)-$shortSha" + } else { + $artifactName = "$branchSlug-$($packageJson.version)-$shortSha" + } + + Write-Output "๐Ÿ“› Artifact name: $artifactName" + + # Expose values as step outputs for downstream steps + "vsix_file=$vsixFileName" >> $env:GITHUB_OUTPUT + "vsix_size=$vsixFileSize" >> $env:GITHUB_OUTPUT + "package_version=$($packageJson.version)" >> $env:GITHUB_OUTPUT + "package_preview=$($packageJson.preview)" >> $env:GITHUB_OUTPUT + "artifact_name=$artifactName" >> $env:GITHUB_OUTPUT + + - name: ๐Ÿ“ค Upload VSIX + id: upload-vsix + if: ${{ !cancelled() && steps.verify-vsix.outcome == 'success' }} + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.verify-vsix.outputs.artifact_name }} + path: ${{ steps.verify-vsix.outputs.vsix_file }} + if-no-files-found: error + compression-level: 0 + + - name: ๐Ÿ“Š Generate Job Summary + if: always() + env: + PACKAGE_VERSION: ${{ steps.verify-vsix.outputs.package_version }} + PACKAGE_PREVIEW: ${{ steps.verify-vsix.outputs.package_preview }} + VSIX_FILE: ${{ steps.verify-vsix.outputs.vsix_file }} + VSIX_SIZE: ${{ steps.verify-vsix.outputs.vsix_size }} + ARTIFACT_NAME: ${{ steps.verify-vsix.outputs.artifact_name }} + ARTIFACT_URL: ${{ steps.upload-vsix.outputs.artifact-url }} + GH_TOKEN: ${{ github.token }} + VALIDATE_RESULT: ${{ steps.validate-version.outcome }} + L10N_RESULT: ${{ steps.l10n.outcome }} + SKILLS_RESULT: ${{ steps.skills.outcome }} + LINT_RESULT: ${{ steps.lint.outcome }} + PRETTIER_RESULT: ${{ steps.prettier.outcome }} + COMPILE_RESULT: ${{ steps.compile.outcome }} + PACKAGE_RESULT: ${{ steps.package.outcome }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + # For pull_request events `github.sha` is the synthetic PR-merge commit. Use head.sha for PRs + # (matches what reviewers see on the PR head); fall back to github.sha for push events. + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + shell: bash + run: | + # โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Each CI workflow (build.yml, test.yml, e2e.yml, nosql-language-service.yml) posts and + # maintains its OWN PR comment, identified by a stable marker. The shell helpers below are + # duplicated across workflows because GitHub Actions has no first-class way to share inline + # shell โ€” keep them in sync when editing. + + result_icon() { + case "$1" in + success) echo "โœ…" ;; + failure) echo "โŒ" ;; + cancelled) echo "โšช" ;; + skipped) echo "โญ๏ธ" ;; + *) echo "โ“" ;; + esac + } + + human_size() { + awk -v b="$1" 'BEGIN { + if (b < 1024) { printf "%d B", b } + else if (b < 1048576) { printf "%.1f KB", b/1024 } + else if (b < 1073741824) { printf "%.2f MB", b/1048576 } + else { printf "%.2f GB", b/1073741824 } + }' + } + + list_run_artifacts() { + local run_id=$1 + local arts + arts=$(gh api --paginate "repos/${REPO}/actions/runs/${run_id}/artifacts" \ + --jq '.artifacts[] | "\(.id)\t\(.name)\t\(.size_in_bytes)\t\(.expired)"' 2>/dev/null || true) + if [ -z "$arts" ]; then + echo "_None_" + return + fi + while IFS=$'\t' read -r id name size expired; do + [ -z "$id" ] && continue + local size_h + size_h=$(human_size "$size") + if [ "$expired" = "true" ]; then + printf -- '- ~~%s~~ โ€” %s _(expired)_\n' "$name" "$size_h" + else + printf -- '- [%s](%s/%s/actions/runs/%s/artifacts/%s) โ€” %s\n' \ + "$name" "$SERVER_URL" "$REPO" "$run_id" "$id" "$size_h" + fi + done <<< "$arts" + } + + post_or_update_comment() { + local pr=$1 + local marker=$2 + local body_file=$3 + + local existing_id + existing_id=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" \ + --jq ".[] | select(.body | contains(\"${marker}\")) | .id" 2>/dev/null | head -1) + + if [ -n "$existing_id" ]; then + if gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ + --field body=@"${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} comment updated (id ${existing_id})" + return 0 + fi + echo "::warning::Failed to PATCH comment ${existing_id}, posting a new one" + fi + + if gh pr comment "${pr}" --repo "${REPO}" --body-file "${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} new comment posted" + return 0 + fi + + echo "::warning::Unable to post PR comment on #${pr} (likely a fork PR with a read-only GITHUB_TOKEN). Skipping." + return 0 + } + + validate_icon=$(result_icon "$VALIDATE_RESULT") + l10n_icon=$(result_icon "$L10N_RESULT") + skills_icon=$(result_icon "$SKILLS_RESULT") + lint_icon=$(result_icon "$LINT_RESULT") + prettier_icon=$(result_icon "$PRETTIER_RESULT") + compile_icon=$(result_icon "$COMPILE_RESULT") + package_icon=$(result_icon "$PACKAGE_RESULT") + build_artifacts=$(list_run_artifacts "${GITHUB_RUN_ID}") + + # Commit link + short_sha="${SHA:0:7}" + commit_link="[${short_sha}](${SERVER_URL}/${REPO}/commit/${SHA})" + + # Resolve PR info: prefer the pull_request event payload, otherwise use `gh pr view` by + # branch name (works for push events too). + if [ -z "${PR_NUMBER:-}" ] && [ -n "${BRANCH_NAME:-}" ]; then + pr_info=$(gh pr view "${BRANCH_NAME}" --repo "${REPO}" \ + --json number,title 2>/dev/null) || true + if [ -n "$pr_info" ]; then + PR_NUMBER=$(echo "$pr_info" | jq -r '.number // empty') + PR_TITLE=$(echo "$pr_info" | jq -r '.title // empty') + echo "โ„น๏ธ Found PR #${PR_NUMBER}: ${PR_TITLE}" + else + echo "โ„น๏ธ No open PR found for branch '${BRANCH_NAME}'" + fi + fi + + # Source section + source_section="## ๐Ÿ”— Source + - **Commit:** ${commit_link}" + if [ -n "${PR_NUMBER:-}" ]; then + source_section="${source_section} + - **Pull Request:** [#${PR_NUMBER} ${PR_TITLE}](${SERVER_URL}/${REPO}/pull/${PR_NUMBER})" + fi + + # Package Information โ€” only render when packaging produced a VSIX. + if [ -n "${PACKAGE_VERSION:-}" ]; then + package_section="## ๐Ÿ“ฆ Package Information + - **Version:** ${PACKAGE_VERSION} + - **Preview:** ${PACKAGE_PREVIEW} + - **VSIX File:** ${VSIX_FILE} + - **VSIX Size:** ${VSIX_SIZE} MB + - **Artifact:** [${ARTIFACT_NAME}.zip](${ARTIFACT_URL})" + else + package_section="## ๐Ÿ“ฆ Package Information + _No VSIX produced โ€” the package step did not succeed (see job results above)._" + fi + + # Overall status โ€” green only when every gating step succeeded or was skipped. verify-skills is + # warning-only and excluded from pass/fail. + all_green=true + for r in "$VALIDATE_RESULT" "$L10N_RESULT" "$LINT_RESULT" \ + "$PRETTIER_RESULT" "$COMPILE_RESULT" "$PACKAGE_RESULT"; do + case "$r" in + success|skipped) ;; + *) all_green=false ;; + esac + done + if [ "$all_green" = "true" ]; then + overall_status="## โœ… Build Status + Compile, analyzers and packaging passed. See sibling comments for unit/integration and E2E results." + else + overall_status="## โŒ Build Status + One or more build checks failed. Every analyzer ran independently โ€” review every โŒ above, not just the first." + fi + + # Write summary โ€” also used verbatim as the PR comment body. + MARKER="" + cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY_EOF + ${MARKER} + # ๐Ÿ”จ Build (Compile, Lint, Prettier, l10n, Package) + + ${source_section} + + ## ๐Ÿงฑ Step Results + - **Validate Version:** ${validate_icon} ${VALIDATE_RESULT} + - **Localization:** ${l10n_icon} ${L10N_RESULT} + - **Lint:** ${lint_icon} ${LINT_RESULT} + - **Prettier:** ${prettier_icon} ${PRETTIER_RESULT} + - **Compile (tsc):** ${compile_icon} ${COMPILE_RESULT} + - **Package:** ${package_icon} ${PACKAGE_RESULT} + - **External Skills (info):** ${skills_icon} ${SKILLS_RESULT} + + ${package_section} + + ## ๐Ÿ“ฅ Artifacts ([run](${SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID})) + ${build_artifacts} + + ${overall_status} + SUMMARY_EOF + + echo "โœ… Job summary written to: ${GITHUB_STEP_SUMMARY}" + + if [ -n "${PR_NUMBER:-}" ]; then + post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" + fi diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d3c9b5453..cf2510597 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,274 +16,278 @@ name: E2E Tests (Webview + Cosmos DB Emulator) permissions: - contents: read - pull-requests: write + contents: read + pull-requests: write on: - workflow_dispatch: + workflow_dispatch: - push: - branches: - - main - - rel/* - - dev/** - paths: - - 'src/**' - - 'test/e2e/**' - - 'scripts/import-seed.mjs' - - 'docker-compose.e2e.yml' - - 'package.json' - - 'package-lock.json' - - 'playwright.config.ts' - - 'tsconfig.e2e.json' - - '.github/workflows/e2e.yml' + push: + branches: + - main + - rel/* + - dev/** + paths: + - 'src/**' + - 'test/e2e/**' + - 'scripts/import-seed.mjs' + - 'docker-compose.e2e.yml' + - 'package.json' + - 'package-lock.json' + - 'playwright.config.ts' + - 'tsconfig.e2e.json' + - '.github/workflows/e2e.yml' - pull_request: - branches: - - main - - rel/* - - dev/** - paths: - - 'src/**' - - 'test/e2e/**' - - 'scripts/import-seed.mjs' - - 'docker-compose.e2e.yml' - - 'package.json' - - 'package-lock.json' - - 'playwright.config.ts' - - 'tsconfig.e2e.json' - - '.github/workflows/e2e.yml' + pull_request: + branches: + - main + - rel/* + - dev/** + paths: + - 'src/**' + - 'test/e2e/**' + - 'scripts/import-seed.mjs' + - 'docker-compose.e2e.yml' + - 'package.json' + - 'package-lock.json' + - 'playwright.config.ts' + - 'tsconfig.e2e.json' + - '.github/workflows/e2e.yml' jobs: - e2e: - name: ๐ŸŽญ Run e2e tests - runs-on: ubuntu-latest - timeout-minutes: 30 + e2e: + name: ๐ŸŽญ Run e2e tests + runs-on: ubuntu-latest + timeout-minutes: 30 - # Same push+pull_request dedupe trick as main.yml: cancelling either run on the same SHA leaves a failed - # required check on the PR head. Skip the pull_request run only for same-repo PRs whose head branch already - # triggers a push run. - if: >- - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name != github.repository || - (github.head_ref != 'main' && - !startsWith(github.head_ref, 'rel/') && - !startsWith(github.head_ref, 'dev/')) + # Same push+pull_request dedupe trick as build.yml: cancelling either run on the same SHA leaves a failed + # required check on the PR head. Skip the pull_request run only for same-repo PRs whose head branch already + # triggers a push run. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository || + (github.head_ref != 'main' && + !startsWith(github.head_ref, 'rel/') && + !startsWith(github.head_ref, 'dev/')) - # Scope concurrency by PR (or by branch on push) so two forks pushing branches with the same name don't cancel - # each other. - concurrency: - group: e2e-${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || github.ref_name }} - cancel-in-progress: true + # Scope concurrency by PR (or by branch on push) so two forks pushing branches with the same name don't cancel + # each other. + concurrency: + group: >- + e2e-${{ github.workflow }}-${{ + github.event_name == 'pull_request' && + format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || + github.ref_name }} + cancel-in-progress: true - steps: - - uses: actions/checkout@v6 + steps: + - uses: actions/checkout@v6 - - name: ๐Ÿ“ฆ Set up Node.js - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - cache: 'npm' - cache-dependency-path: '**/package-lock.json' + - name: ๐Ÿ“ฆ Set up Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'npm' + cache-dependency-path: '**/package-lock.json' - - name: โฌ‡๏ธ Install dependencies - run: npm ci + - name: โฌ‡๏ธ Install dependencies + run: npm ci - - name: ๐Ÿ”ง Install Electron system dependencies - # Provides libgtk-3, libnss3, libasound2, etc โ€” required by the VS Code Electron build that - # @vscode/test-electron downloads. `chromium` is the cheapest dep set that covers Electron too. - run: npx playwright install-deps chromium + - name: ๐Ÿ”ง Install Electron system dependencies + # Provides libgtk-3, libnss3, libasound2, etc โ€” required by the VS Code Electron build that + # @vscode/test-electron downloads. `chromium` is the cheapest dep set that covers Electron too. + run: npx playwright install-deps chromium - - name: ๐Ÿ”จ Build extension - # globalSetup auto-rebuilds when dist/ is stale, but doing it explicitly here makes build failures show - # up as a distinct step in the run log instead of buried inside the e2e step. - run: npm run vite-prod + - name: ๐Ÿ”จ Build extension + # globalSetup auto-rebuilds when dist/ is stale, but doing it explicitly here makes build failures show + # up as a distinct step in the run log instead of buried inside the e2e step. + run: npm run vite-prod - - name: ๐Ÿงช Run e2e tests - id: e2e-tests - # xvfb-run is mandatory on Linux: Electron has no real headless mode and the runner has no display. - # `-a` picks the next free display number. Retries (CI: 2) come from playwright.config.ts. - # COSMOSDB_E2E_SKIP_BUILD=1 โ€” dist/ is already fresh from the build step above; the freshness check in - # globalSetup would walk src/ for no gain. - env: - COSMOSDB_E2E_SKIP_BUILD: '1' - run: xvfb-run -a npm run e2e + - name: ๐Ÿงช Run e2e tests + id: e2e-tests + # xvfb-run is mandatory on Linux: Electron has no real headless mode and the runner has no display. + # `-a` picks the next free display number. Retries (CI: 2) come from playwright.config.ts. + # COSMOSDB_E2E_SKIP_BUILD=1 โ€” dist/ is already fresh from the build step above; the freshness check in + # globalSetup would walk src/ for no gain. + env: + COSMOSDB_E2E_SKIP_BUILD: '1' + run: xvfb-run -a npm run e2e - - name: ๐Ÿ“ค Upload Playwright HTML report - if: always() - uses: actions/upload-artifact@v7 - with: - name: e2e-html-report-${{ github.run_attempt }} - # Per-run report lives at test/e2e/.reports//html/ (runId is random per - # invocation โ€” see test/e2e/helpers/e2eIsolation.ts). Two non-obvious requirements: - # 1. `include-hidden-files: true` โ€” `.reports` starts with a dot, and the action's - # default glob walker skips hidden directories entirely, so without this every - # pattern under `.reports/` resolves to zero matches. - # 2. Trailing `/**` โ€” @actions/glob matches files, not bare directories, so - # `**/html` on its own would not pick up files inside the html/ folder either. - path: test/e2e/.reports/**/html/** - include-hidden-files: true - if-no-files-found: warn - retention-days: 14 + - name: ๐Ÿ“ค Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@v7 + with: + name: e2e-html-report-${{ github.run_attempt }} + # Per-run report lives at test/e2e/.reports//html/ (runId is random per + # invocation โ€” see test/e2e/helpers/e2eIsolation.ts). Two non-obvious requirements: + # 1. `include-hidden-files: true` โ€” `.reports` starts with a dot, and the action's + # default glob walker skips hidden directories entirely, so without this every + # pattern under `.reports/` resolves to zero matches. + # 2. Trailing `/**` โ€” @actions/glob matches files, not bare directories, so + # `**/html` on its own would not pick up files inside the html/ folder either. + path: test/e2e/.reports/**/html/** + include-hidden-files: true + if-no-files-found: warn + retention-days: 14 - - name: ๐Ÿ“ค Upload Playwright results (traces, videos, screenshots) - # `!cancelled()` (not just `failure()`) so we also capture traces/videos when a - # flaky test fails the first attempt and passes on retry โ€” in that case the job - # conclusion is `success`, but the trace from the failed attempt is still on disk - # and is the most useful thing to look at. `if-no-files-found: ignore` keeps the - # log clean on fully green runs where Playwright wrote nothing here. - # `include-hidden-files: true` is required because the directory is `.results/`. - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 - with: - name: e2e-results-${{ github.run_attempt }} - path: test/e2e/.results/** - include-hidden-files: true - if-no-files-found: ignore - retention-days: 14 + - name: ๐Ÿ“ค Upload Playwright results (traces, videos, screenshots) + # `!cancelled()` (not just `failure()`) so we also capture traces/videos when a + # flaky test fails the first attempt and passes on retry โ€” in that case the job + # conclusion is `success`, but the trace from the failed attempt is still on disk + # and is the most useful thing to look at. `if-no-files-found: ignore` keeps the + # log clean on fully green runs where Playwright wrote nothing here. + # `include-hidden-files: true` is required because the directory is `.results/`. + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: e2e-results-${{ github.run_attempt }} + path: test/e2e/.results/** + include-hidden-files: true + if-no-files-found: ignore + retention-days: 14 - - name: ๐Ÿ“ค Upload emulator logs on failure - if: failure() - run: | - mkdir -p emulator-logs - docker logs cosmosdb-e2e-cosmosdb-emulator-1 > emulator-logs/cosmosdb-emulator.log 2>&1 || true - continue-on-error: true + - name: ๐Ÿ“ค Upload emulator logs on failure + if: failure() + run: | + mkdir -p emulator-logs + docker logs cosmosdb-e2e-cosmosdb-emulator-1 > emulator-logs/cosmosdb-emulator.log 2>&1 || true + continue-on-error: true - - name: ๐Ÿ“ค Upload emulator logs artifact - if: failure() - uses: actions/upload-artifact@v7 - with: - name: e2e-emulator-logs-${{ github.run_attempt }} - path: emulator-logs - if-no-files-found: warn - retention-days: 14 + - name: ๐Ÿ“ค Upload emulator logs artifact + if: failure() + uses: actions/upload-artifact@v7 + with: + name: e2e-emulator-logs-${{ github.run_attempt }} + path: emulator-logs + if-no-files-found: warn + retention-days: 14 - - name: ๐Ÿ“Š Generate Job Summary & PR comment - # Each CI workflow now owns its own PR comment (identified by a stable HTML-comment - # marker). main.yml posts the build/lint/unit/integration summary; this step posts - # the e2e slice โ€” test result + downloadable HTML report. The marker lets re-runs - # update the existing comment in-place rather than spamming the PR. - if: always() - env: - GH_TOKEN: ${{ github.token }} - SERVER_URL: ${{ github.server_url }} - REPO: ${{ github.repository }} - GITHUB_RUN_ID: ${{ github.run_id }} - E2E_RESULT: ${{ steps.e2e-tests.outcome }} - # Use head.sha for PRs so the displayed SHA matches what reviewers see on the - # PR head, not the synthetic merge commit github.sha resolves to for PR events. - SHA: ${{ github.event.pull_request.head.sha || github.sha }} - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - shell: bash - run: | - result_icon() { - case "$1" in - success) echo "โœ…" ;; - failure) echo "โŒ" ;; - cancelled) echo "โšช" ;; - skipped) echo "โญ๏ธ" ;; - *) echo "โ“" ;; - esac - } - human_size() { - awk -v b="$1" 'BEGIN { - if (b < 1024) { printf "%d B", b } - else if (b < 1048576) { printf "%.1f KB", b/1024 } - else if (b < 1073741824) { printf "%.2f MB", b/1048576 } - else { printf "%.2f GB", b/1073741824 } - }' - } - # See main.yml for the rationale behind list_run_artifacts / - # post_or_update_comment โ€” identical logic, duplicated because GitHub Actions - # has no first-class way to share inline shell across workflows. - list_run_artifacts() { - local run_id=$1 - local arts - arts=$(gh api --paginate "repos/${REPO}/actions/runs/${run_id}/artifacts" \ - --jq '.artifacts[] | "\(.id)\t\(.name)\t\(.size_in_bytes)\t\(.expired)"' 2>/dev/null || true) - if [ -z "$arts" ]; then - echo "_None_" - return - fi - while IFS=$'\t' read -r id name size expired; do - [ -z "$id" ] && continue - local size_h - size_h=$(human_size "$size") - if [ "$expired" = "true" ]; then - printf -- '- ~~%s~~ โ€” %s _(expired)_\n' "$name" "$size_h" - else - printf -- '- [%s](%s/%s/actions/runs/%s/artifacts/%s) โ€” %s\n' \ - "$name" "$SERVER_URL" "$REPO" "$run_id" "$id" "$size_h" - fi - done <<< "$arts" - } - post_or_update_comment() { - local pr=$1 - local marker=$2 - local body_file=$3 - local existing_id - existing_id=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" \ - --jq ".[] | select(.body | contains(\"${marker}\")) | .id" 2>/dev/null | head -1) - if [ -n "$existing_id" ]; then - if gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ - --field body=@"${body_file}" >/dev/null 2>&1; then - echo "โœ… PR #${pr} comment updated (id ${existing_id})" - return 0 - fi - echo "::warning::Failed to PATCH comment ${existing_id}, posting a new one" - fi - if gh pr comment "${pr}" --repo "${REPO}" --body-file "${body_file}" >/dev/null 2>&1; then - echo "โœ… PR #${pr} new comment posted" - return 0 - fi - echo "::warning::Unable to post PR comment on #${pr} (likely a fork PR with a read-only GITHUB_TOKEN). Skipping." + - name: ๐Ÿ“Š Generate Job Summary & PR comment + # Each CI workflow now owns its own PR comment (identified by a stable HTML-comment + # marker). build.yml posts the compile/lint/prettier/l10n/package summary, test.yml posts + # the unit/integration summary; this step posts the e2e slice โ€” test result + downloadable + # HTML report. The marker lets re-runs update the existing comment in-place rather than + # spamming the PR. + if: always() + env: + GH_TOKEN: ${{ github.token }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + E2E_RESULT: ${{ steps.e2e-tests.outcome }} + # Use head.sha for PRs so the displayed SHA matches what reviewers see on the + # PR head, not the synthetic merge commit github.sha resolves to for PR events. + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + shell: bash + run: | + result_icon() { + case "$1" in + success) echo "โœ…" ;; + failure) echo "โŒ" ;; + cancelled) echo "โšช" ;; + skipped) echo "โญ๏ธ" ;; + *) echo "โ“" ;; + esac + } + human_size() { + awk -v b="$1" 'BEGIN { + if (b < 1024) { printf "%d B", b } + else if (b < 1048576) { printf "%.1f KB", b/1024 } + else if (b < 1073741824) { printf "%.2f MB", b/1048576 } + else { printf "%.2f GB", b/1073741824 } + }' + } + # See build.yml for the rationale behind list_run_artifacts / + # post_or_update_comment โ€” identical logic, duplicated because GitHub Actions + # has no first-class way to share inline shell across workflows. + list_run_artifacts() { + local run_id=$1 + local arts + arts=$(gh api --paginate "repos/${REPO}/actions/runs/${run_id}/artifacts" \ + --jq '.artifacts[] | "\(.id)\t\(.name)\t\(.size_in_bytes)\t\(.expired)"' 2>/dev/null || true) + if [ -z "$arts" ]; then + echo "_None_" + return + fi + while IFS=$'\t' read -r id name size expired; do + [ -z "$id" ] && continue + local size_h + size_h=$(human_size "$size") + if [ "$expired" = "true" ]; then + printf -- '- ~~%s~~ โ€” %s _(expired)_\n' "$name" "$size_h" + else + printf -- '- [%s](%s/%s/actions/runs/%s/artifacts/%s) โ€” %s\n' \ + "$name" "$SERVER_URL" "$REPO" "$run_id" "$id" "$size_h" + fi + done <<< "$arts" + } + post_or_update_comment() { + local pr=$1 + local marker=$2 + local body_file=$3 + local existing_id + existing_id=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" \ + --jq ".[] | select(.body | contains(\"${marker}\")) | .id" 2>/dev/null | head -1) + if [ -n "$existing_id" ]; then + if gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ + --field body=@"${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} comment updated (id ${existing_id})" return 0 - } - - short_sha="${SHA:0:7}" - commit_link="[${short_sha}](${SERVER_URL}/${REPO}/commit/${SHA})" - e2e_icon=$(result_icon "$E2E_RESULT") - artifacts=$(list_run_artifacts "${GITHUB_RUN_ID}") - - # Resolve PR info for push events (no PR payload) by branch name. - if [ -z "${PR_NUMBER:-}" ] && [ -n "${BRANCH_NAME:-}" ]; then - pr_info=$(gh pr view "${BRANCH_NAME}" --repo "${REPO}" \ - --json number,title 2>/dev/null) || true - if [ -n "$pr_info" ]; then - PR_NUMBER=$(echo "$pr_info" | jq -r '.number // empty') - PR_TITLE=$(echo "$pr_info" | jq -r '.title // empty') - fi fi + echo "::warning::Failed to PATCH comment ${existing_id}, posting a new one" + fi + if gh pr comment "${pr}" --repo "${REPO}" --body-file "${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} new comment posted" + return 0 + fi + echo "::warning::Unable to post PR comment on #${pr} (likely a fork PR with a read-only GITHUB_TOKEN). Skipping." + return 0 + } - source_section="**Commit:** ${commit_link}" - if [ -n "${PR_NUMBER:-}" ]; then - source_section="${source_section} - **Pull Request:** [#${PR_NUMBER} ${PR_TITLE}](${SERVER_URL}/${REPO}/pull/${PR_NUMBER})" - fi + short_sha="${SHA:0:7}" + commit_link="[${short_sha}](${SERVER_URL}/${REPO}/commit/${SHA})" + e2e_icon=$(result_icon "$E2E_RESULT") + artifacts=$(list_run_artifacts "${GITHUB_RUN_ID}") - MARKER="" - cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY_EOF - ${MARKER} - # ๐ŸŽญ E2E Tests (Playwright + VS Code) + # Resolve PR info for push events (no PR payload) by branch name. + if [ -z "${PR_NUMBER:-}" ] && [ -n "${BRANCH_NAME:-}" ]; then + pr_info=$(gh pr view "${BRANCH_NAME}" --repo "${REPO}" \ + --json number,title 2>/dev/null) || true + if [ -n "$pr_info" ]; then + PR_NUMBER=$(echo "$pr_info" | jq -r '.number // empty') + PR_TITLE=$(echo "$pr_info" | jq -r '.title // empty') + fi + fi - ${source_section} + source_section="**Commit:** ${commit_link}" + if [ -n "${PR_NUMBER:-}" ]; then + source_section="${source_section} + **Pull Request:** [#${PR_NUMBER} ${PR_TITLE}](${SERVER_URL}/${REPO}/pull/${PR_NUMBER})" + fi - ## ๐Ÿงช Result - - **E2E Tests:** ${e2e_icon} ${E2E_RESULT} + MARKER="" + cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY_EOF + ${MARKER} + # ๐ŸŽญ E2E Tests (Playwright + VS Code) - ## ๐Ÿ“ฅ Artifacts ([run](${SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID})) - ${artifacts} + ${source_section} - > Tip: the HTML report artifact contains a self-contained Playwright report. - > Download the zip, extract, and open \`index.html\` โ€” or run - > \`npx playwright show-report \` for the interactive view. - SUMMARY_EOF + ## ๐Ÿงช Result + - **E2E Tests:** ${e2e_icon} ${E2E_RESULT} - echo "โœ… Job summary written to: ${GITHUB_STEP_SUMMARY}" + ## ๐Ÿ“ฅ Artifacts ([run](${SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID})) + ${artifacts} - if [ -n "${PR_NUMBER:-}" ]; then - post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" - fi + > Tip: the HTML report artifact contains a self-contained Playwright report. + > Download the zip, extract, and open \`index.html\` โ€” or run + > \`npx playwright show-report \` for the interactive view. + SUMMARY_EOF + + echo "โœ… Job summary written to: ${GITHUB_STEP_SUMMARY}" + if [ -n "${PR_NUMBER:-}" ]; then + post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" + fi diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index fe51280c4..388d08771 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -1,50 +1,50 @@ -name: Feature Request Manager -on: - issues: - types: [milestoned] - schedule: - - cron: 15 5 * * * # 10:15pm PT - workflow_dispatch: - -# The triage action writes via AZCODE_BOT_PAT, so GITHUB_TOKEN only needs to read repo contents for `actions/checkout`. -# Explicit minimal permissions (CodeQL actions/missing-workflow-permissions). -permissions: - contents: read - -jobs: - main: - name: โœจ Triage feature requests - runs-on: ubuntu-latest - steps: - - name: Checkout Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: actions/checkout@v3 - with: - repository: "microsoft/vscode-github-triage-actions" - path: ./actions - ref: stable - - name: Install Actions - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - run: npm install --production --prefix ./actions - - name: Run Feature Request Manager - if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') - uses: ./actions/feature-request - with: - token: ${{secrets.AZCODE_BOT_PAT}} - owner: "microsoft" - repo: "vscode-cosmosdb" - candidateMilestoneID: 28 - candidateMilestoneName: "Backlog Candidates" - backlogMilestoneID: 33 - featureRequestLabel: "feature" - upvotesRequired: 5 - numCommentsOverride: 10 - initComment: "This feature request is now a candidate for our backlog. The community has 240 days to upvote the issue. If it receives 5 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" - rejectComment: ":slightly_frowning_face: In the last 60 days, this issue has received less than 5 community upvotes and we closed it. Still a big Thank You to you for taking the time to create it! To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" - rejectLabel: "out of scope" - warnComment: "This issue has become stale and is at risk of being closed. The community has 60 days to upvote the issue. If it receives 5 upvotes we will keep it open and take another look. If not, we will close it. To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" - labelsToExclude: "P0,P1" - acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" - warnDays: 60 - closeDays: 240 - milestoneDelaySeconds: 60 +name: Feature Request Manager +on: + issues: + types: [milestoned] + schedule: + - cron: 15 5 * * * # 10:15pm PT + workflow_dispatch: + +# The triage action writes via AZCODE_BOT_PAT, so GITHUB_TOKEN only needs to read repo contents for `actions/checkout`. +# Explicit minimal permissions (CodeQL actions/missing-workflow-permissions). +permissions: + contents: read + +jobs: + main: + name: โœจ Triage feature requests + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') + uses: actions/checkout@v3 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + ref: stable + - name: Install Actions + if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') + run: npm install --production --prefix ./actions + - name: Run Feature Request Manager + if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') + uses: ./actions/feature-request + with: + token: ${{secrets.AZCODE_BOT_PAT}} + owner: 'microsoft' + repo: 'vscode-cosmosdb' + candidateMilestoneID: 28 + candidateMilestoneName: 'Backlog Candidates' + backlogMilestoneID: 33 + featureRequestLabel: 'feature' + upvotesRequired: 5 + numCommentsOverride: 10 + initComment: "This feature request is now a candidate for our backlog. The community has 240 days to upvote the issue. If it receives 5 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" + rejectComment: ":slightly_frowning_face: In the last 60 days, this issue has received less than 5 community upvotes and we closed it. Still a big Thank You to you for taking the time to create it! To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" + rejectLabel: 'out of scope' + warnComment: "This issue has become stale and is at risk of being closed. The community has 60 days to upvote the issue. If it receives 5 upvotes we will keep it open and take another look. If not, we will close it. To learn more about how we handle issues, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" + labelsToExclude: 'P0,P1' + acceptComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/azcodeissuetriaging).\n\nHappy Coding!" + warnDays: 60 + closeDays: 240 + milestoneDelaySeconds: 60 diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index c69b08d05..74b0d8fc0 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout Actions uses: actions/checkout@v2 with: - repository: "microsoft/vscode-github-triage-actions" + repository: 'microsoft/vscode-github-triage-actions' path: ./actions ref: stable - name: Install Actions diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 04ad0646d..ddb687193 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout Actions uses: actions/checkout@v2 with: - repository: "microsoft/vscode-github-triage-actions" + repository: 'microsoft/vscode-github-triage-actions' path: ./actions ref: stable - name: Install Actions diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 0eb3b429e..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,432 +0,0 @@ -name: Node PR Lint, Build and Test - -permissions: - contents: read - pull-requests: write - # Needed by the summary step to look up the conclusion of sibling - # workflows (e2e.yml, nosql-language-service.yml) for the same commit. - actions: read - code-quality: write -on: - # Trigger when manually run - workflow_dispatch: - - # Trigger on pushes to `main`, `rel/*`, or `dev/**` - push: - branches: - - main - - rel/* - - dev/** - - # Trigger on pull requests to `main`, `rel/*`, or `dev/**` - pull_request: - branches: - - main - - rel/* - - dev/** - -jobs: - Build: - runs-on: ubuntu-latest - - # A push to a branch with an open PR fires both `push` and `pull_request` - # events. Rather than cancelling one of them (a cancelled run reports - # as a failed required check on the PR's head SHA and blocks merge), - # we skip the `pull_request` run for same-repo PRs only when the PR - # head branch is also covered by this workflow's `push.branches` - # filters, because only those branches already get a `push` run in - # this repo. For PRs from forks, or same-repo PRs from other head - # branches such as `feature/*`, we still let `pull_request` run. - if: >- - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name != github.repository || - (github.head_ref != 'main' && - !startsWith(github.head_ref, 'rel/') && - !startsWith(github.head_ref, 'dev/')) - - # Concurrency only dedupes runs for the SAME branch across DIFFERENT - # commits (e.g. rapid pushes during a rebase). The cancelled older run - # is on a stale SHA that is no longer the PR head, so it does not - # block the PR. Push and pull_request events for the same SHA are - # already deduped by the `if` above, so they never collide here. - # For `pull_request` events, scope the group by PR number and head - # repo so that two forks pushing branches with the same name do not - # cancel each other. - concurrency: - group: build-${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || github.ref_name }} - cancel-in-progress: true - - defaults: - run: - working-directory: '.' - - steps: - # Setup - - uses: actions/checkout@v6 - - - name: ๐Ÿ“ฆ Using Node.js - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - cache: 'npm' - cache-dependency-path: '**/package-lock.json' - - - name: โš–๏ธ Validate Version / Preview Alignment - shell: pwsh - run: | - $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json - $version = $packageJson.version - $preview = [bool]$packageJson.preview - - Write-Output "๐Ÿ“ฆ Package version: $version" - Write-Output "๐Ÿท๏ธ Preview flag: $preview" - - # Validate semantic versioning format - if ($version -notmatch '^(\d+)\.(\d+)\.(\d+)$') { - Write-Error "โŒ Invalid semantic version format: $version" - exit 1 - } - - $minor = [int]$Matches[2] - $minorEven = ($minor % 2) -eq 0 - - # Check version/preview alignment - # Convention: odd minor versions = preview, even minor versions = stable - if ($preview -and $minorEven) { - Write-Warning "โš ๏ธ Version/preview misalignment: preview=$preview but minor version $minor is even (should be odd for preview)" - Write-Warning "Convention: Odd minor versions (e.g., 1.1.x, 1.3.x) should be preview, even (e.g., 1.0.x, 1.2.x) should be stable" - } elseif (-not $preview -and -not $minorEven) { - Write-Warning "โš ๏ธ Version/preview misalignment: preview=$preview but minor version $minor is odd (should be even for stable)" - Write-Warning "Convention: Odd minor versions (e.g., 1.1.x, 1.3.x) should be preview, even (e.g., 1.0.x, 1.2.x) should be stable" - } else { - Write-Output "โœ… Version/preview alignment is correct" - } - - - name: โฌ‡๏ธ Install Dependencies - run: npm ci - - - name: ๐ŸŒ Localize - run: npm run l10n:check - - - name: ๐Ÿ” Verify External Skills - shell: bash - run: | - cp -r skills/ skills-backup/ - npm run fetch-skill - # Use --strip-trailing-cr -B -b to ignore line ending, blank line, and whitespace differences - if ! diff -rq --strip-trailing-cr -B -b skills/ skills-backup/ > /dev/null 2>&1; then - echo "::warning::Committed skills/ does not match the pinned commit in package.json. If you have local skill changes, submit them to the upstream skill repository first, then update the pinned commit in package.json and run 'npm run fetch-skill' to pull the latest version." - diff -r --strip-trailing-cr -B -b skills/ skills-backup/ || true - else - echo "โœ… External skills are up to date." - fi - rm -rf skills/ - mv skills-backup/ skills/ - - - name: ๐Ÿงน Lint - run: npm run lint - - - name: โœจ Prettier - run: npm run prettier - - - name: ๐Ÿ”จ Compile - run: npm run build - - - name: ๐Ÿ“ฆ Package - run: npm run package - - - name: ๐Ÿ” Verify VSIX File - id: verify-vsix - shell: pwsh - run: | - # Find VSIX file - $vsixFiles = Get-ChildItem -Path . -Filter *.vsix -File - - if ($vsixFiles.Count -eq 0) { - Write-Error "โŒ No VSIX file found" - exit 1 - } elseif ($vsixFiles.Count -gt 1) { - Write-Error "โŒ Multiple VSIX files found: $($vsixFiles.Name -join ', ')" - exit 1 - } - - $vsixFile = $vsixFiles[0] - $vsixFileName = $vsixFile.Name - $vsixFileSize = [math]::Round($vsixFile.Length / 1MB, 2) - - Write-Output "โœ… Found VSIX: $vsixFileName (${vsixFileSize} MB)" - - # Verify filename matches expected pattern - $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json - $expectedName = "$($packageJson.name)-$($packageJson.version).vsix" - - if ($vsixFileName -ne $expectedName) { - Write-Error "โŒ VSIX filename mismatch: expected '$expectedName', got '$vsixFileName'" - exit 1 - } - - Write-Output "โœ… VSIX filename is correct: $vsixFileName" - - # Compute a short commit SHA (7 chars) and a filesystem-safe branch slug. - # Use head_ref for pull_request events, otherwise ref_name. - $rawRef = if ("${{ github.event_name }}" -eq "pull_request") { "${{ github.head_ref }}" } else { "${{ github.ref_name }}" } - $branchSlug = ($rawRef -replace '[^A-Za-z0-9._-]', '-').Trim('-') - $shortSha = "${{ github.sha }}".Substring(0, 7) - - # Artifact name convention: - # - main / rel/* : -- - # - everything else: -- - $isRelease = ($rawRef -eq 'main') -or ($rawRef -like 'rel/*') - if ($isRelease) { - $artifactName = "$($packageJson.name)-$($packageJson.version)-$shortSha" - } else { - $artifactName = "$branchSlug-$($packageJson.version)-$shortSha" - } - - Write-Output "๐Ÿ“› Artifact name: $artifactName" - - # Expose values as step outputs for downstream steps - "vsix_file=$vsixFileName" >> $env:GITHUB_OUTPUT - "vsix_size=$vsixFileSize" >> $env:GITHUB_OUTPUT - "package_version=$($packageJson.version)" >> $env:GITHUB_OUTPUT - "package_preview=$($packageJson.preview)" >> $env:GITHUB_OUTPUT - "artifact_name=$artifactName" >> $env:GITHUB_OUTPUT - - - name: ๐Ÿ“ค Upload VSIX - id: upload-vsix - uses: actions/upload-artifact@v7 - with: - name: ${{ steps.verify-vsix.outputs.artifact_name }} - path: ${{ steps.verify-vsix.outputs.vsix_file }} - if-no-files-found: error - compression-level: 0 - - - name: ๐Ÿงช Unit Tests - id: unit-tests - run: npm run vitest:coverage - - - name: ๐Ÿ“Š Upload Code Coverage - if: ${{ !cancelled() && steps.unit-tests.outcome != 'skipped' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - uses: actions/upload-code-coverage@v1 - with: - file: coverage/cobertura-coverage.xml - language: TypeScript - label: code-coverage/vitest - - - name: ๐Ÿงช Integration Tests - id: integration-tests - # Run even when Unit Tests failed (we want to see both buckets in the summary), but skip when an earlier - # build step failed โ€” without `dist/` from the Compile step there's nothing to integration-test against. - if: ${{ !cancelled() && steps.upload-vsix.outcome == 'success' }} - run: | - exit_code=1 - for i in 1 2 3; do - xvfb-run -a npm test - exit_code=$? - if [ $exit_code -eq 0 ]; then break; fi - echo "Attempt $i failed with exit code $exit_code" - if [ $i -lt 3 ]; then sleep 15; fi - done - exit $exit_code - - - name: ๐Ÿ“Š Generate Job Summary - if: always() - env: - PACKAGE_VERSION: ${{ steps.verify-vsix.outputs.package_version }} - PACKAGE_PREVIEW: ${{ steps.verify-vsix.outputs.package_preview }} - VSIX_FILE: ${{ steps.verify-vsix.outputs.vsix_file }} - VSIX_SIZE: ${{ steps.verify-vsix.outputs.vsix_size }} - ARTIFACT_NAME: ${{ steps.verify-vsix.outputs.artifact_name }} - ARTIFACT_URL: ${{ steps.upload-vsix.outputs.artifact-url }} - GH_TOKEN: ${{ github.token }} - UNIT_TEST_RESULT: ${{ steps.unit-tests.outcome }} - INTEGRATION_TEST_RESULT: ${{ steps.integration-tests.outcome }} - SERVER_URL: ${{ github.server_url }} - REPO: ${{ github.repository }} - GITHUB_RUN_ID: ${{ github.run_id }} - # For pull_request events `github.sha` is the synthetic PR-merge commit. - # Use head.sha for PRs (matches what reviewers see on the PR head); fall - # back to github.sha for push events where there is only one SHA. - SHA: ${{ github.event.pull_request.head.sha || github.sha }} - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - shell: bash - run: | - # Only generate summary if VSIX verification completed - if [ -z "${PACKAGE_VERSION:-}" ]; then - echo "โš ๏ธ Skipping job summary - VSIX verification did not complete" - exit 0 - fi - - # โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # Each CI workflow (main.yml, e2e.yml, nosql-language-service.yml) now posts and - # maintains *its own* PR comment, identified by a stable marker. main.yml no - # longer polls siblings โ€” that approach was fragile (PR-merge SHA mismatch, - # 60-second skip-window, ~5โ€“10 min wait for siblings). Each workflow now owns - # its slice of the PR conversation: build details + VSIX here; e2e results + - # report there; nosql integration result in its own comment. - - result_icon() { - case "$1" in - success) echo "โœ…" ;; - failure) echo "โŒ" ;; - cancelled) echo "โšช" ;; - skipped) echo "โญ๏ธ" ;; - *) echo "โ“" ;; - esac - } - - # Pretty-print one byte size value (KB / MB / GB). - human_size() { - awk -v b="$1" 'BEGIN { - if (b < 1024) { printf "%d B", b } - else if (b < 1048576) { printf "%.1f KB", b/1024 } - else if (b < 1073741824) { printf "%.2f MB", b/1048576 } - else { printf "%.2f GB", b/1073741824 } - }' - } - - # Emit a Markdown bullet list of all artifacts attached to a run_id. Links - # use the GitHub UI URL which triggers a zip download for any logged-in user. - # Outputs "_None_" when the run has no artifacts. - list_run_artifacts() { - local run_id=$1 - local arts - arts=$(gh api --paginate "repos/${REPO}/actions/runs/${run_id}/artifacts" \ - --jq '.artifacts[] | "\(.id)\t\(.name)\t\(.size_in_bytes)\t\(.expired)"' 2>/dev/null || true) - if [ -z "$arts" ]; then - echo "_None_" - return - fi - while IFS=$'\t' read -r id name size expired; do - [ -z "$id" ] && continue - local size_h - size_h=$(human_size "$size") - if [ "$expired" = "true" ]; then - printf -- '- ~~%s~~ โ€” %s _(expired)_\n' "$name" "$size_h" - else - printf -- '- [%s](%s/%s/actions/runs/%s/artifacts/%s) โ€” %s\n' \ - "$name" "$SERVER_URL" "$REPO" "$run_id" "$id" "$size_h" - fi - done <<< "$arts" - } - - # Post or update a PR comment identified by an HTML-comment marker. More - # robust than `gh pr comment --edit-last`, which would clobber whichever - # comment happened to be most recent โ€” including comments from sibling - # workflows. We list the PR's issue comments via gh api and PATCH the one - # whose body contains our marker, falling back to a new comment otherwise. - post_or_update_comment() { - local pr=$1 - local marker=$2 - local body_file=$3 - - local existing_id - existing_id=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" \ - --jq ".[] | select(.body | contains(\"${marker}\")) | .id" 2>/dev/null | head -1) - - if [ -n "$existing_id" ]; then - if gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ - --field body=@"${body_file}" >/dev/null 2>&1; then - echo "โœ… PR #${pr} comment updated (id ${existing_id})" - return 0 - fi - echo "::warning::Failed to PATCH comment ${existing_id}, posting a new one" - fi - - if gh pr comment "${pr}" --repo "${REPO}" --body-file "${body_file}" >/dev/null 2>&1; then - echo "โœ… PR #${pr} new comment posted" - return 0 - fi - - echo "::warning::Unable to post PR comment on #${pr} (likely a fork PR with a read-only GITHUB_TOKEN). Skipping." - return 0 - } - - unit_icon=$(result_icon "$UNIT_TEST_RESULT") - int_icon=$(result_icon "$INTEGRATION_TEST_RESULT") - build_artifacts=$(list_run_artifacts "${GITHUB_RUN_ID}") - - # Commit link - short_sha="${SHA:0:7}" - commit_link="[${short_sha}](${SERVER_URL}/${REPO}/commit/${SHA})" - - # Resolve PR info: prefer the pull_request event payload, otherwise - # use `gh pr view` by branch name (works for push events too). - if [ -z "${PR_NUMBER:-}" ] && [ -n "${BRANCH_NAME:-}" ]; then - pr_info=$(gh pr view "${BRANCH_NAME}" --repo "${REPO}" \ - --json number,title 2>/dev/null) || true - if [ -n "$pr_info" ]; then - PR_NUMBER=$(echo "$pr_info" | jq -r '.number // empty') - PR_TITLE=$(echo "$pr_info" | jq -r '.title // empty') - echo "โ„น๏ธ Found PR #${PR_NUMBER}: ${PR_TITLE}" - else - echo "โ„น๏ธ No open PR found for branch '${BRANCH_NAME}'" - fi - fi - - # Source section - source_section="## ๐Ÿ”— Source - - **Commit:** ${commit_link}" - if [ -n "${PR_NUMBER:-}" ]; then - source_section="${source_section} - - **Pull Request:** [#${PR_NUMBER} ${PR_TITLE}](${SERVER_URL}/${REPO}/pull/${PR_NUMBER})" - fi - - # Overall status โ€” green only when our two local test buckets succeeded - # OR were skipped (an earlier step failed and skipped them). - all_green=true - for r in "$UNIT_TEST_RESULT" "$INTEGRATION_TEST_RESULT"; do - case "$r" in - success|skipped) ;; - *) all_green=false ;; - esac - done - if [ "$all_green" = "true" ]; then - overall_status="## โœ… Build Status - Build and local tests passed. See sibling comments below for E2E and NoSQL integration results." - else - overall_status="## โŒ Build Status - Some checks failed. Please review the logs." - fi - - # Write summary โ€” also used verbatim as the PR comment body. - MARKER="" - cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY_EOF - ${MARKER} - # ๐Ÿ”จ Build, Lint & Test - - ${source_section} - - ## ๐Ÿ“ฆ Package Information - - **Version:** ${PACKAGE_VERSION} - - **Preview:** ${PACKAGE_PREVIEW} - - **VSIX File:** ${VSIX_FILE} - - **VSIX Size:** ${VSIX_SIZE} MB - - **Artifact:** [${ARTIFACT_NAME}.zip](${ARTIFACT_URL}) - - ## ๐Ÿงช Test Results - - **Unit Tests:** ${unit_icon} ${UNIT_TEST_RESULT} - - **Integration Tests (extension host):** ${int_icon} ${INTEGRATION_TEST_RESULT} - - ## ๐Ÿ“ฅ Artifacts ([run](${SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID})) - ${build_artifacts} - - ${overall_status} - SUMMARY_EOF - - echo "โœ… Job summary written to: ${GITHUB_STEP_SUMMARY}" - - if [ -n "${PR_NUMBER:-}" ]; then - post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" - fi - - # Fail the summary step only if a local test bucket actually failed. "skipped" means an earlier - # step (Lint / Compile / โ€ฆ) already failed the job, so flagging it again here just adds noise. - for r in "$UNIT_TEST_RESULT" "$INTEGRATION_TEST_RESULT"; do - case "$r" in - success|skipped) ;; - *) exit 1 ;; - esac - done diff --git a/.github/workflows/nosql-language-service.yml b/.github/workflows/nosql-language-service.yml index 04ff65e64..5e499d803 100644 --- a/.github/workflows/nosql-language-service.yml +++ b/.github/workflows/nosql-language-service.yml @@ -1,12 +1,12 @@ # Workflow scope: `packages/nosql-language-service` integration tests. # -# This is NOT the extension-host integration suite under `test/` โ€” that one runs inside `main.yml` ("Build, lint & -# test"). This workflow covers the **NoSQL language service package** (`packages/nosql-language-service`), which talks +# This is NOT the extension-host integration suite under `test/` โ€” that one runs inside `test.yml` ("Tests"). +# This workflow covers the **NoSQL language service package** (`packages/nosql-language-service`), which talks # to a real Cosmos DB Emulator over the wire to validate that its query fixtures behave the same against a live # backend as they do in the unit tests. # # Why a separate workflow: -# - It needs Docker (Cosmos emulator) โ€” `main.yml` does not. +# - It needs Docker (Cosmos emulator) โ€” `build.yml` / `test.yml` do not. # - It only matters when `packages/**` or the seed script changes, so the path filter below keeps the run off the # vast majority of commits. # @@ -20,23 +20,52 @@ permissions: pull-requests: write on: + workflow_dispatch: + push: - branches: [main] + branches: + - main + - rel/* + - dev/** paths: - 'packages/**' - 'scripts/import-seed.mjs' - '.github/workflows/nosql-language-service.yml' pull_request: + branches: + - main + - rel/* + - dev/** paths: - 'packages/**' - 'scripts/import-seed.mjs' - '.github/workflows/nosql-language-service.yml' +# Workflow-level concurrency dedupes rapid pushes on the same branch; PR runs are scoped by PR number + head +# repo so two forks pushing branches with the same name don't cancel each other. +concurrency: + group: >- + nosql-${{ github.workflow }}-${{ + github.event_name == 'pull_request' && + format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || + github.ref_name }} + cancel-in-progress: true + jobs: integration: - name: ๐Ÿงช NoSQL language-service integration + name: NoSQL language-service integration runs-on: ubuntu-latest + # Same push+pull_request dedupe trick as build.yml: a same-repo branch covered by `push.branches` fires + # both events on one commit. Skip the pull_request run for those (the push run provides the checks); fork + # PRs and other head branches still run on pull_request. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository || + (github.head_ref != 'main' && + !startsWith(github.head_ref, 'rel/') && + !startsWith(github.head_ref, 'dev/')) + env: COSMOS_ENDPOINT: https://localhost:8081 NODE_TLS_REJECT_UNAUTHORIZED: '0' @@ -101,7 +130,9 @@ jobs: # external dashboards / dorny/test-reporter ingest test-level results. # `--outputFile.junit=โ€ฆ` selects which reporter receives the path (needed when # multiple reporters are configured; the unsuffixed `--outputFile` is ambiguous). - run: npx vitest run --reporter=verbose --reporter=junit --outputFile.junit=junit.xml src/test-fixtures/integration.test.ts + run: >- + npx vitest run --reporter=verbose --reporter=junit + --outputFile.junit=junit.xml src/test-fixtures/integration.test.ts working-directory: packages/nosql-language-service - name: ๐Ÿ“ค Upload JUnit report @@ -156,7 +187,7 @@ jobs: else { printf "%.2f GB", b/1073741824 } }' } - # See main.yml for the rationale โ€” duplicated because GitHub Actions has no + # See build.yml for the rationale โ€” duplicated because GitHub Actions has no # first-class way to share inline shell across workflows. list_run_artifacts() { local run_id=$1 @@ -241,4 +272,3 @@ jobs: if [ -n "${PR_NUMBER:-}" ]; then post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" fi - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..1a7647a9e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,206 @@ +# Tests workflow โ€” unit tests (vitest) and integration tests (VS Code extension host), as sequential steps in +# a single job. +# +# Design: +# - This whole workflow runs IN PARALLEL with build.yml (compile/lint/prettier/l10n/package) on a separate +# runner, because the tests are the slow part and shouldn't wait on the fast analyzers. +# - Within this job the steps are sequential: unit first (fast), then build dist/ + integration (slow). The +# integration step uses `if: ${{ !cancelled() }}` so it still runs when unit tests fail โ€” that way one run +# reports BOTH buckets instead of hiding the integration result behind a unit failure. +# - No Docker/emulator is required: the only integration test is extension activation, which makes no database +# connections (see test/extensionActivation.test.ts). E2E (Playwright + Cosmos emulator) lives in e2e.yml. + +name: Tests + +permissions: + contents: read + pull-requests: write + # Needed by the summary step's gh api calls (list this run's artifacts). + actions: read + # Needed by the Upload Code Coverage step (actions/upload-code-coverage). + code-quality: write + +on: + workflow_dispatch: + + push: + branches: + - main + - rel/* + - dev/** + + pull_request: + branches: + - main + - rel/* + - dev/** + +concurrency: + group: >- + test-${{ github.workflow }}-${{ + github.event_name == 'pull_request' && + format('{0}-{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.number) || + github.ref_name }} + cancel-in-progress: true + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + + # Same push+pull_request dedupe trick as build.yml โ€” see the comment there. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository || + (github.head_ref != 'main' && + !startsWith(github.head_ref, 'rel/') && + !startsWith(github.head_ref, 'dev/')) + + steps: + - uses: actions/checkout@v6 + + - name: ๐Ÿ“ฆ Using Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + + - name: โฌ‡๏ธ Install Dependencies + run: npm ci + + - name: ๐Ÿงช Unit Tests + id: unit-tests + run: npm run vitest:coverage + + - name: ๐Ÿ“Š Upload Code Coverage + # Skip on fork PRs (no token to upload) and when unit tests never ran. `!cancelled()` so coverage from + # a passing unit run is still uploaded even though later steps (build/integration) continue. + if: >- + ${{ !cancelled() && steps.unit-tests.outcome != 'skipped' && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) }} + uses: actions/upload-code-coverage@v1 + with: + file: coverage/cobertura-coverage.xml + language: TypeScript + label: code-coverage/vitest + + - name: ๐Ÿ”จ Build extension + # `npm test` loads the extension from dist/; build it explicitly so a build failure surfaces as its own + # step rather than being buried inside the test runner. `!cancelled()` so we still get here (and run + # integration) when unit tests failed. + if: ${{ !cancelled() }} + run: npm run vite-prod + + - name: ๐Ÿงช Integration Tests + id: integration-tests + # Run even when unit tests failed so a single run reports both buckets. xvfb-run is mandatory on Linux: + # VS Code/Electron has no real headless mode and the runner has no display. `-a` picks the next free + # display number. Retry up to 3 times to ride out the occasional flaky extension-host startup. + if: ${{ !cancelled() }} + run: | + exit_code=1 + for i in 1 2 3; do + xvfb-run -a npm test + exit_code=$? + if [ $exit_code -eq 0 ]; then break; fi + echo "Attempt $i failed with exit code $exit_code" + if [ $i -lt 3 ]; then sleep 15; fi + done + exit $exit_code + + - name: ๐Ÿ“Š Generate Job Summary + if: always() + env: + GH_TOKEN: ${{ github.token }} + UNIT_TEST_RESULT: ${{ steps.unit-tests.outcome }} + INTEGRATION_TEST_RESULT: ${{ steps.integration-tests.outcome }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + # Use head.sha for PRs so the displayed SHA matches the PR head, not the synthetic merge commit. + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + shell: bash + run: | + # See build.yml for the rationale behind these helpers โ€” duplicated because GitHub Actions has no + # first-class way to share inline shell across workflows. Keep them in sync. + result_icon() { + case "$1" in + success) echo "โœ…" ;; + failure) echo "โŒ" ;; + cancelled) echo "โšช" ;; + skipped) echo "โญ๏ธ" ;; + *) echo "โ“" ;; + esac + } + + post_or_update_comment() { + local pr=$1 + local marker=$2 + local body_file=$3 + + local existing_id + existing_id=$(gh api --paginate "repos/${REPO}/issues/${pr}/comments" \ + --jq ".[] | select(.body | contains(\"${marker}\")) | .id" 2>/dev/null | head -1) + + if [ -n "$existing_id" ]; then + if gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ + --field body=@"${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} comment updated (id ${existing_id})" + return 0 + fi + echo "::warning::Failed to PATCH comment ${existing_id}, posting a new one" + fi + + if gh pr comment "${pr}" --repo "${REPO}" --body-file "${body_file}" >/dev/null 2>&1; then + echo "โœ… PR #${pr} new comment posted" + return 0 + fi + + echo "::warning::Unable to post PR comment on #${pr} (likely a fork PR with a read-only GITHUB_TOKEN). Skipping." + return 0 + } + + unit_icon=$(result_icon "$UNIT_TEST_RESULT") + int_icon=$(result_icon "$INTEGRATION_TEST_RESULT") + + short_sha="${SHA:0:7}" + commit_link="[${short_sha}](${SERVER_URL}/${REPO}/commit/${SHA})" + + # Resolve PR info for push events (no PR payload) by branch name. + if [ -z "${PR_NUMBER:-}" ] && [ -n "${BRANCH_NAME:-}" ]; then + pr_info=$(gh pr view "${BRANCH_NAME}" --repo "${REPO}" \ + --json number,title 2>/dev/null) || true + if [ -n "$pr_info" ]; then + PR_NUMBER=$(echo "$pr_info" | jq -r '.number // empty') + PR_TITLE=$(echo "$pr_info" | jq -r '.title // empty') + fi + fi + + source_section="**Commit:** ${commit_link}" + if [ -n "${PR_NUMBER:-}" ]; then + source_section="${source_section} + **Pull Request:** [#${PR_NUMBER} ${PR_TITLE}](${SERVER_URL}/${REPO}/pull/${PR_NUMBER})" + fi + + MARKER="" + cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY_EOF + ${MARKER} + # ๐Ÿงช Tests (Unit + Integration) + + ${source_section} + + ## ๐Ÿงช Results + - **Unit Tests (vitest):** ${unit_icon} ${UNIT_TEST_RESULT} + - **Integration Tests (extension host):** ${int_icon} ${INTEGRATION_TEST_RESULT} + SUMMARY_EOF + + echo "โœ… Job summary written to: ${GITHUB_STEP_SUMMARY}" + + if [ -n "${PR_NUMBER:-}" ]; then + post_or_update_comment "${PR_NUMBER}" "${MARKER}" "${GITHUB_STEP_SUMMARY}" + fi diff --git a/.oxfmtrc.json b/.oxfmtrc.json index a02e53e77..3b70e6cbf 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -24,19 +24,24 @@ }, "overrides": [ { - "files": ["*.md", "*.json", "*.jsonc", "*.yml", "*.yaml"], + "files": ["*.md", "*.json", "*.jsonc"], "options": { "tabWidth": 2, "trailingComma": "none", "singleQuote": false } + }, + { + "files": ["*.yml", "*.yaml"], + "options": { + "tabWidth": 2 + } } ], "ignorePatterns": [ ".azure-pipelines", ".config", ".cosmosdb-data", - ".github", ".vscode-test", "bundle-analysis", "coverage", diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index bb1e0441d..1207c0138 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -23,8 +23,8 @@ services: cosmosdb-emulator: image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview ports: - - "8082:8081" # Main endpoint (mapped off the default to avoid clashes) - - "1235:1234" # Additional service port (mapped off the default) + - '8082:8081' # Main endpoint (mapped off the default to avoid clashes) + - '1235:1234' # Additional service port (mapped off the default) environment: PROTOCOL: https-insecure NODE_TLS_REJECT_UNAUTHORIZED: 0 diff --git a/docker-compose.yml b/docker-compose.yml index 3e57ba426..11bd8be00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,8 +23,8 @@ services: cosmosdb-init: condition: service_completed_successfully ports: - - "8081:8081" # Main endpoint for Cosmos DB operations - - "1234:1234" # Additional service port + - '8081:8081' # Main endpoint for Cosmos DB operations + - '1234:1234' # Additional service port environment: # Available values: https, http, https-insecure PROTOCOL: https-insecure