diff --git a/.github/workflows/actions/build-and-package/action.yaml b/.github/workflows/actions/build-and-package/action.yaml new file mode 100644 index 000000000..19196e84e --- /dev/null +++ b/.github/workflows/actions/build-and-package/action.yaml @@ -0,0 +1,163 @@ +name: Check Build and Package +description: Run checks, build and package VSIX, sign it, and run security scans (Ubuntu only) +inputs: + SEGMENT_KEY: + description: Segment analytics key + required: true + ARTIFACTORY_HOST: + description: Artifactory host for signing + required: true + ARTIFACTORY_PASSWORD: + description: Artifactory password for signing + required: true + ARTIFACTORY_USERNAME: + description: Artifactory username for signing + required: true + GARASIGN_PASSWORD: + description: Garasign password for signing + required: true + GARASIGN_USERNAME: + description: Garasign username for signing + required: true + SNYK_TOKEN: + description: Snyk token for security scanning + required: true + JIRA_API_TOKEN: + description: Jira API token for vulnerability tickets + required: true + +runs: + using: "composite" + steps: + - name: Install Deps Ubuntu + run: sudo apt-get update -y && sudo apt-get -y install libkrb5-dev libsecret-1-dev net-tools libstdc++6 gnome-keyring + shell: bash + + # Default Python (3.12) doesn't have support for distutils because of + # which the dep install fails constantly on macos + # https://github.com/nodejs/node-gyp/issues/2869 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Run node-gyp bug workaround script + run: | + curl -sSfLO https://raw.githubusercontent.com/mongodb-js/compass/42e6142ae08be6fec944b80ff6289e6bcd11badf/.evergreen/node-gyp-bug-workaround.sh && bash node-gyp-bug-workaround.sh + shell: bash + + - name: Set SEGMENT_KEY + env: + SEGMENT_KEY: ${{ inputs.SEGMENT_KEY }} + run: | + echo "SEGMENT_KEY=${SEGMENT_KEY}" >> $GITHUB_ENV + shell: bash + + - name: Validate SEGMENT_KEY + run: | + if [ -z "${SEGMENT_KEY}" ]; then + echo "SEGMENT_KEY is not set or is empty" + exit 1 + fi + shell: bash + + - name: Install Dependencies + shell: bash + run: | + # Retry npm ci up to 3 times to handle transient network errors + for i in 1 2 3; do + npm ci --omit=optional && break || { + if [ $i -eq 3 ]; then + echo "npm ci failed after 3 attempts" + exit 1 + fi + echo "npm ci failed, retrying in 10 seconds... (attempt $i/3)" + sleep 10 + } + done + + - name: Compile + run: npm run compile + shell: bash + + - name: Run Checks + run: npm run check + shell: bash + + - name: Build .vsix + env: + NODE_OPTIONS: "--require ./scripts/no-npm-list-fail.js --max_old_space_size=4096" + # NOTE: --githubBranch is "The GitHub branch used to infer relative links in README.md." + run: | + npx vsce package --githubBranch main + shell: bash + + - name: Check .vsix filesize + run: npm run check-vsix-size + shell: bash + + - name: Sign .vsix + env: + ARTIFACTORY_PASSWORD: ${{ inputs.ARTIFACTORY_PASSWORD }} + ARTIFACTORY_USERNAME: ${{ inputs.ARTIFACTORY_USERNAME }} + GARASIGN_PASSWORD: ${{ inputs.GARASIGN_PASSWORD }} + GARASIGN_USERNAME: ${{ inputs.GARASIGN_USERNAME }} + run: | + set -e + FILE_TO_SIGN=$(find . -maxdepth 1 -name '*.vsix' -print -quit) + if [ -z "$FILE_TO_SIGN" ]; then + echo "Error: No .vsix file found in the current directory." >&2 + exit 1 + fi + node scripts/sign-vsix.js "${FILE_TO_SIGN}" + ls *.vsix.sig + shell: bash + + - name: Prepare artifact upload + shell: bash + run: | + echo "Files to be uploaded:" + ls -lh *.vsix *.vsix.sig + echo "" + echo "VSIX checksum (SHA256):" + sha256sum *.vsix + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: VSIX Package + path: | + *.vsix + *.vsix.sig + + - name: Run Snyk Test + shell: bash + env: + SNYK_TOKEN: ${{ inputs.SNYK_TOKEN }} + run: | + npm run snyk-test + + - name: Create Jira Tickets + if: > + ( + github.event_name == 'push' && github.ref == 'refs/heads/main' || + github.event_name == 'workflow_dispatch' || + github.event_name == 'schedule' + ) + shell: bash + env: + JIRA_API_TOKEN: ${{ inputs.JIRA_API_TOKEN }} + JIRA_BASE_URL: "https://jira.mongodb.org" + JIRA_PROJECT: "VSCODE" + JIRA_VULNERABILITY_BUILD_INFO: "- [GitHub Run|https://github.com/mongodb-js/vscode/actions/runs/${{github.run_id}}/jobs/${{github.job}}]" + run: | + npm run create-vulnerability-tickets > /dev/null + + - name: Generate Vulnerability Report (Fail on >= High) + continue-on-error: ${{ github.event_name == 'pull_request' }} + shell: bash + run: | + # The standard output is suppressed since Github Actions logs are + # available for everyone with read access to the repo, which is everyone that is + # logged in for public repos. + # This command is only here to fail on failures for `main` and tags. + npm run generate-vulnerability-report > /dev/null diff --git a/.github/workflows/actions/run-tests/action.yaml b/.github/workflows/actions/run-tests/action.yaml new file mode 100644 index 000000000..249408e82 --- /dev/null +++ b/.github/workflows/actions/run-tests/action.yaml @@ -0,0 +1,224 @@ +name: Run Tests +description: Run checks, tests, and install tests on the VSIX package +inputs: + SEGMENT_KEY: + description: Segment analytics key + required: true + +runs: + using: "composite" + steps: + - name: Install Deps Ubuntu + if: ${{ runner.os == 'Linux' }} + run: sudo apt-get update -y && sudo apt-get -y install libkrb5-dev libsecret-1-dev net-tools libstdc++6 gnome-keyring + shell: bash + + # Default Python (3.12) doesn't have support for distutils because of + # which the dep install fails constantly on macos + # https://github.com/nodejs/node-gyp/issues/2869 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Run node-gyp bug workaround script + run: | + curl -sSfLO https://raw.githubusercontent.com/mongodb-js/compass/42e6142ae08be6fec944b80ff6289e6bcd11badf/.evergreen/node-gyp-bug-workaround.sh && bash node-gyp-bug-workaround.sh + shell: bash + + - name: Set SEGMENT_KEY + env: + SEGMENT_KEY: ${{ inputs.SEGMENT_KEY }} + run: | + echo "SEGMENT_KEY=${SEGMENT_KEY}" >> $GITHUB_ENV + shell: bash + + - name: Validate SEGMENT_KEY + run: | + if [ -z "${SEGMENT_KEY}" ]; then + echo "SEGMENT_KEY is not set or is empty" + exit 1 + fi + shell: bash + + - name: Install Dependencies + shell: bash + run: | + # Retry npm ci up to 3 times to handle transient network errors + for i in 1 2 3; do + npm ci --omit=optional && break || { + if [ $i -eq 3 ]; then + echo "npm ci failed after 3 attempts" + exit 1 + fi + echo "npm ci failed, retrying in 10 seconds... (attempt $i/3)" + sleep 10 + } + done + + - name: Download VSIX artifact + uses: actions/download-artifact@v4 + with: + name: VSIX Package + + - name: Verify VSIX artifact + shell: bash + run: | + VSIX_FILE=$(find . -maxdepth 1 -name '*.vsix' -print -quit) + if [ -z "$VSIX_FILE" ]; then + echo "Error: No .vsix file found after artifact download" >&2 + exit 1 + fi + echo "Testing on ${{ runner.os }}: $VSIX_FILE" + ls -lh "$VSIX_FILE" + echo "" + echo "VSIX checksum (SHA256):" + if [ "${{ runner.os }}" == "macOS" ]; then + shasum -a 256 "$VSIX_FILE" + else + sha256sum "$VSIX_FILE" + fi + + - name: Run Tests + env: + NODE_OPTIONS: "--max_old_space_size=4096" + MDB_IS_TEST: "true" + run: | + npm run test + shell: bash + + - name: Install VSIX and Test + shell: bash + run: | + set -e + + echo "VSIX Installation Test - ${{ runner.os }}" + echo "" + + # Find the VSIX file (downloaded from build-and-package artifact) + VSIX_FILE=$(find . -maxdepth 1 -name '*.vsix' -print -quit) + if [ -z "$VSIX_FILE" ]; then + echo "Error: No .vsix file found" >&2 + exit 1 + fi + + echo "Found VSIX file: $VSIX_FILE" + + # Verify the file exists and is readable + if [ ! -r "$VSIX_FILE" ]; then + echo "Error: VSIX file is not readable" >&2 + exit 1 + fi + + echo "Installing: $VSIX_FILE" + ls -lh "$VSIX_FILE" + + # Determine VS Code CLI command based on OS + if [ "${{ runner.os }}" == "macOS" ]; then + VSCODE_CLI="/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" + if [ ! -f "$VSCODE_CLI" ]; then + echo "Installing VS Code on macOS..." + if ! command -v brew &> /dev/null; then + echo "Error: Homebrew is not installed" >&2 + exit 1 + fi + brew install --cask visual-studio-code + sleep 5 + if [ ! -f "$VSCODE_CLI" ]; then + echo "Error: VS Code installation failed" >&2 + exit 1 + fi + fi + + elif [ "${{ runner.os }}" == "Windows" ]; then + if command -v code &> /dev/null; then + VSCODE_CLI="code" + else + echo "Installing VS Code on Windows..." + if ! command -v choco &> /dev/null; then + echo "Error: Chocolatey is not installed" >&2 + exit 1 + fi + choco install vscode -y --no-progress + + POSSIBLE_PATHS=( + "/c/Program Files/Microsoft VS Code/bin/code" + "/c/Program Files (x86)/Microsoft VS Code/bin/code" + "$LOCALAPPDATA/Programs/Microsoft VS Code/bin/code" + ) + + VSCODE_CLI="" + for path in "${POSSIBLE_PATHS[@]}"; do + if [ -f "$path" ] || [ -f "${path}.cmd" ]; then + VSCODE_CLI="code" + export PATH="$PATH:$(dirname "$path")" + break + fi + done + + if [ -z "$VSCODE_CLI" ]; then + export PATH="$PATH:/c/Program Files/Microsoft VS Code/bin" + if command -v code &> /dev/null; then + VSCODE_CLI="code" + else + echo "Error: VS Code installation failed or not found in PATH" >&2 + exit 1 + fi + fi + sleep 5 + fi + + else + if command -v code &> /dev/null; then + VSCODE_CLI="code" + else + echo "Installing VS Code on Linux..." + wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg + sudo install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg + echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | sudo tee /etc/apt/sources.list.d/vscode.list > /dev/null + rm -f packages.microsoft.gpg + sudo apt-get update + sudo apt-get install -y code + if ! command -v code &> /dev/null; then + echo "Error: VS Code installation failed" >&2 + exit 1 + fi + VSCODE_CLI="code" + sleep 5 + fi + fi + + "$VSCODE_CLI" --version + + EXTENSION_ID=$(node -p "const pkg = require('./package.json'); pkg.publisher + '.' + pkg.name") + echo "Extension ID: $EXTENSION_ID" + + echo "Installing extension..." + "$VSCODE_CLI" --install-extension "$VSIX_FILE" --force + sleep 2 + + echo "Verifying installation..." + INSTALLED_EXTENSIONS=$("$VSCODE_CLI" --list-extensions) + + if echo "$INSTALLED_EXTENSIONS" | grep -q "$EXTENSION_ID"; then + INSTALLED_VERSION=$("$VSCODE_CLI" --list-extensions --show-versions | grep "$EXTENSION_ID") + echo "✓ Installed: $INSTALLED_VERSION" + else + echo "Error: Extension not found in installed extensions list" >&2 + echo "Expected: $EXTENSION_ID" + echo "Installed extensions:" + echo "$INSTALLED_EXTENSIONS" + exit 1 + fi + + echo "Uninstalling extension..." + "$VSCODE_CLI" --uninstall-extension "$EXTENSION_ID" + sleep 2 + + INSTALLED_EXTENSIONS_AFTER=$("$VSCODE_CLI" --list-extensions) + if echo "$INSTALLED_EXTENSIONS_AFTER" | grep -q "$EXTENSION_ID"; then + echo "Warning: Extension still appears after uninstall" + else + echo "✓ Uninstalled successfully" + fi + + echo "✓ VSIX installation test completed" diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 0a5cf2990..b64d31a28 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -1,34 +1,35 @@ +# Run manually to prepare a draft release for the next version of the extension. +# The workflow will create a draft github release where the .vsix can be +# downloaded and manually tested before publishing. To release the version, +# publish the draft release, which will trigger the publish-release workflow. name: Draft release on: workflow_dispatch: inputs: versionBump: - description: 'Version bump' + description: "Version bump" type: choice required: true - default: 'patch' + default: "patch" options: - - patch - - minor - - major - - exact-version + - patch + - minor + - major + - exact-version exactVersion: description: 'Exact version: (Only effective selecting "exact-version" as version bump)' required: false -description: | - Run manually to prepare a draft release for the next version of the extension. The workflow will create a draft - github release where the .vsix can be downloaded and manually tested before publishing. To release the version, - publish the draft release, which will trigger the publish-release workflow. - permissions: contents: write jobs: prepare-release: runs-on: ubuntu-latest + outputs: + release-tag: ${{ steps.set-tag.outputs.release-tag }} steps: - name: Checkout uses: actions/checkout@v4 @@ -85,8 +86,40 @@ jobs: exit 1 fi - - name: Run tests and build - uses: ./.github/workflows/actions/test-and-build + - name: Set release tag output + id: set-tag + run: echo "release-tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT + + - name: Upload updated package.json + uses: actions/upload-artifact@v4 + with: + name: updated-package-json + path: package.json + + build-and-package: + name: Check, Build and Package + runs-on: ubuntu-latest + needs: prepare-release + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download updated package.json + uses: actions/download-artifact@v4 + with: + name: updated-package-json + + - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: 22.15.1 + cache: npm + + - name: Check, build and package + uses: ./.github/workflows/actions/build-and-package with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} @@ -96,11 +129,58 @@ jobs: GARASIGN_USERNAME: ${{ secrets.GARASIGN_USERNAME }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - MDB_IS_TEST: "true" + + test: + name: Test + needs: build-and-package + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + fail-fast: false + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download updated package.json + uses: actions/download-artifact@v4 + with: + name: updated-package-json + + - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: 22.15.1 + cache: npm + + - name: Run tests + uses: ./.github/workflows/actions/run-tests + with: + SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} + + create-draft-release: + name: Create Draft Release + runs-on: ubuntu-latest + needs: [prepare-release, test] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download VSIX artifact + uses: actions/download-artifact@v4 + with: + name: VSIX Package - name: Create Draft Release run: | set -e + RELEASE_TAG="${{ needs.prepare-release.outputs.release-tag }}" echo Creating draft release for: "${RELEASE_TAG}" ls *.vsix ls *.vsix.sig @@ -115,4 +195,3 @@ jobs: shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/test-and-build-from-fork.yaml b/.github/workflows/test-and-build-from-fork.yaml index 62493f282..40ce5fd69 100644 --- a/.github/workflows/test-and-build-from-fork.yaml +++ b/.github/workflows/test-and-build-from-fork.yaml @@ -7,8 +7,40 @@ permissions: contents: read jobs: - test-and-build: - name: Test and Build + build-and-package: + name: Check, Build and Package + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.head.repo.full_name != github.repository + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: 22.15.1 + cache: npm + + - name: Check, build and package + uses: ./.github/workflows/actions/build-and-package + with: + SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_DEV }} + ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + GARASIGN_PASSWORD: ${{ secrets.GARASIGN_PASSWORD }} + GARASIGN_USERNAME: ${{ secrets.GARASIGN_USERNAME }} + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + test: + name: Test + needs: build-and-package if: github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.head.repo.full_name != github.repository strategy: @@ -28,17 +60,39 @@ jobs: node-version: 22.15.1 cache: npm - - name: Install Dependencies - run: npm ci --omit=optional - - - name: Run Checks - run: npm run check - # the glob here just fails - if: ${{ runner.os != 'Windows' }} + - name: Run tests + uses: ./.github/workflows/actions/run-tests + with: + SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_DEV }} - - name: Run Tests + merge-dependabot-pr: + name: Merge Dependabot PR + runs-on: ubuntu-latest + needs: + - test + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" env: - NODE_OPTIONS: "--max_old_space_size=4096" - SEGMENT_KEY: "test-segment-key" - MDB_IS_TEST: "true" - run: npm run test + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + status-check: + name: Test and Build + runs-on: ubuntu-latest + needs: [build-and-package, test] + if: always() + steps: + - name: Check job results + run: | + if [ "${{ needs.build-and-package.result }}" == "failure" ] || [ "${{ needs.test.result }}" == "failure" ]; then + echo "One or more jobs failed" + exit 1 + elif [ "${{ needs.build-and-package.result }}" == "cancelled" ] || [ "${{ needs.test.result }}" == "cancelled" ]; then + echo "One or more jobs were cancelled" + exit 1 + else + echo "All jobs completed successfully or were skipped" + exit 0 + fi diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index 4aa0e1b29..03afae562 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -14,8 +14,38 @@ permissions: contents: read jobs: - test-and-build: - name: Test and Build + build-and-package: + name: Check, Build and Package + runs-on: ubuntu-latest + if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: 22.15.1 + cache: npm + + - name: Check, build and package + uses: ./.github/workflows/actions/build-and-package + with: + SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} + ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + GARASIGN_PASSWORD: ${{ secrets.GARASIGN_PASSWORD }} + GARASIGN_USERNAME: ${{ secrets.GARASIGN_USERNAME }} + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + test: + name: Test + needs: build-and-package if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository strategy: @@ -37,8 +67,8 @@ jobs: node-version: 22.15.1 cache: npm - - name: Run tests and build - uses: ./.github/workflows/actions/test-and-build + - name: Run tests + uses: ./.github/workflows/actions/run-tests with: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_PROD }} ARTIFACTORY_HOST: ${{ secrets.ARTIFACTORY_HOST }} @@ -48,4 +78,22 @@ jobs: GARASIGN_USERNAME: ${{ secrets.GARASIGN_USERNAME }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - MDB_IS_TEST: "true" + + status-check: + name: Test and Build + runs-on: ubuntu-latest + needs: [build-and-package, test] + if: always() + steps: + - name: Check job results + run: | + if [ "${{ needs.build-and-package.result }}" == "failure" ] || [ "${{ needs.test.result }}" == "failure" ]; then + echo "One or more jobs failed" + exit 1 + elif [ "${{ needs.build-and-package.result }}" == "cancelled" ] || [ "${{ needs.test.result }}" == "cancelled" ]; then + echo "One or more jobs were cancelled" + exit 1 + else + echo "All jobs completed successfully or were skipped" + exit 0 + fi