diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 0000000..6fa06bf --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,200 @@ +name: Pull Request Tests + +permissions: + contents: read # Needed for checkout and caching + checks: write # Needed for publishing test results as check runs + pull-requests: write # Needed for publishing test results to PR + +on: + pull_request: + branches: + - main + - dev + types: [opened, synchronize, reopened] + paths: + - '**.cs' + - '**.csproj' + - '**.xaml' + - '**.vsct' + - '**.vsixmanifest' + - '.github/workflows/pr-tests.yml' + +# Cancel in-progress runs when new commits are pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + # Cache NuGet packages to speed up builds + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore NuGet packages + run: | + nuget restore InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj + nuget restore InterfaceExtractor.Tests/InterfaceExtractor.Tests.csproj + shell: pwsh + + # Build with warnings as errors for quality + - name: Build Extension + run: msbuild InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj /p:Configuration=Debug /v:minimal /m /warnaserror + + - name: Build Tests + run: msbuild InterfaceExtractor.Tests/InterfaceExtractor.Tests.csproj /p:Configuration=Debug /v:minimal /m /warnaserror + + - name: Setup VSTest + uses: darenm/Setup-VSTest@v1.2 + + - name: Run Tests + shell: pwsh + run: | + # Find the test DLL (might be in net48 subdirectory) + $testDll = Get-ChildItem -Path "InterfaceExtractor.Tests\bin\Debug" -Filter "InterfaceExtractor.Tests.dll" -Recurse | Select-Object -First 1 + + if ($null -eq $testDll) { + Write-Host "โŒ Test DLL not found!" + exit 1 + } + + Write-Host "โœ… Found test DLL at: $($testDll.FullName)" + + # Run tests + vstest.console.exe $testDll.FullName /logger:trx /ResultsDirectory:TestResults + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/windows@v2 + if: always() + with: + files: | + TestResults/**/*.trx + check_name: Test Results + comment_title: Test Results + compare_to_earlier_commit: false + + - name: Upload Test Results as Artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: TestResults/ + retention-days: 7 + + - name: Test Summary + if: always() + shell: pwsh + run: | + $trxFiles = Get-ChildItem -Path TestResults -Filter *.trx -Recurse + + if ($trxFiles.Count -eq 0) { + Write-Host "โŒ No test results found!" + exit 1 + } + + foreach ($trxFile in $trxFiles) { + [xml]$trx = Get-Content $trxFile.FullName + $summary = $trx.TestRun.ResultSummary + $counters = $summary.Counters + + $total = [int]$counters.total + $passed = [int]$counters.passed + $failed = [int]$counters.failed + $skipped = [int]$counters.total - [int]$counters.executed + + Write-Host "" + Write-Host "๐Ÿ“Š Test Summary" + Write-Host "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + Write-Host "โœ… Passed: $passed" + Write-Host "โŒ Failed: $failed" + Write-Host "โญ๏ธ Skipped: $skipped" + Write-Host "๐Ÿ“ Total: $total" + Write-Host "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + if ($failed -gt 0) { + Write-Host "" + Write-Host "โŒ Tests Failed!" + exit 1 + } else { + Write-Host "" + Write-Host "โœ… All Tests Passed!" + } + } + + code-quality: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + # Cache NuGet packages + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore NuGet packages + run: | + nuget restore InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj + nuget restore InterfaceExtractor.Tests/InterfaceExtractor.Tests.csproj + shell: pwsh + + - name: Build Solution (Release) + run: msbuild InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj /p:Configuration=Release /v:minimal /m /warnaserror + + - name: Check for Build Warnings + shell: pwsh + run: | + Write-Host "โœ… Build completed successfully with no warnings!" + + status-check: + runs-on: ubuntu-latest + needs: [test, code-quality] + if: always() + + steps: + - name: Check Test Status + if: needs.test.result != 'success' + run: | + echo "โŒ Tests failed or were cancelled" + exit 1 + + - name: Check Code Quality Status + if: needs.code-quality.result != 'success' + run: | + echo "โŒ Code quality checks failed or were cancelled" + exit 1 + + - name: All Checks Passed + run: | + echo "โœ… All checks passed successfully!" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f45e4eb..f0b455f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,38 @@ name: Visual Studio Extension Release + +permissions: + contents: write # Needed for creating releases and pushing tags + on: push: branches: - main - dev + paths: + - '**.cs' + - '**.csproj' + - '**.xaml' + - '**.vsct' + - '**.vsixmanifest' + - '.github/workflows/release.yml' + +# Prevent multiple releases from running simultaneously +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false # Don't cancel releases, queue them jobs: build-and-publish: runs-on: windows-latest + + # Require environment approval based on branch + environment: + name: ${{ github.ref_name == 'main' && 'production' || 'development' }} + url: https://github.com/${{ github.repository }}/releases + permissions: contents: write + steps: - uses: actions/checkout@v4 with: @@ -22,6 +45,17 @@ jobs: - name: Setup NuGet uses: NuGet/setup-nuget@v2 + # Cache NuGet packages + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: | + ~/.nuget/packages + ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Restore NuGet packages run: nuget restore InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj @@ -66,92 +100,134 @@ jobs: - name: Auto-increment version id: version - shell: bash + shell: pwsh run: | - # Read current version from vsixmanifest - MANIFEST_FILE="InterfaceExtractor.Extension/source.extension.vsixmanifest" - CURRENT_VERSION=$(grep -oP 'Version="\K[^"]+' "$MANIFEST_FILE" | head -1) - BUMP_TYPE="${{ steps.version-bump.outputs.BUMP_TYPE }}" - BRANCH_NAME="${{ github.ref_name }}" + # Read current version from vsixmanifest using PowerShell XML parsing + $manifestFile = "InterfaceExtractor.Extension/source.extension.vsixmanifest" + + # Load XML properly + [xml]$manifest = Get-Content $manifestFile + $currentVersion = $manifest.PackageManifest.Metadata.Identity.Version + + if ([string]::IsNullOrEmpty($currentVersion)) { + Write-Host "โŒ Could not read version from manifest" + exit 1 + } + + Write-Host "Current version: $currentVersion" + + $bumpType = "${{ steps.version-bump.outputs.BUMP_TYPE }}" + $branchName = "${{ github.ref_name }}" # Parse version components - IFS='.' read -r MAJOR MINOR PATCH BUILD <<< "$CURRENT_VERSION" - - # Determine new version based on bump type (for main branch) - if [[ "$BUMP_TYPE" == "major" ]]; then - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - elif [[ "$BUMP_TYPE" == "minor" ]]; then - MINOR=$((MINOR + 1)) - PATCH=0 - else - PATCH=$((PATCH + 1)) - fi + $versionParts = $currentVersion.Split('.') + $major = [int]$versionParts[0] + $minor = [int]$versionParts[1] + $patch = [int]$versionParts[2] - BASE_VERSION="${MAJOR}.${MINOR}.${PATCH}" + # Determine new version based on bump type + if ($bumpType -eq "major") { + $major++ + $minor = 0 + $patch = 0 + } + elseif ($bumpType -eq "minor") { + $minor++ + $patch = 0 + } + else { + $patch++ + } + + $baseVersion = "$major.$minor.$patch" # Determine prerelease type based on branch - if [[ "$BRANCH_NAME" == *"testing"* ]]; then + if ($branchName -like "*testing*") { # Testing branches: find the next testing prerelease number - LAST_TESTING_TAG=$(git tag -l "v${BASE_VERSION}-testing.*" --sort=-version:refname | head -n 1) - if [ -z "$LAST_TESTING_TAG" ]; then - PRERELEASE_NUM=0 - else - LAST_NUM=$(echo "$LAST_TESTING_TAG" | sed -n 's/.*-testing\.\([0-9]*\).*/\1/p') - PRERELEASE_NUM=$((LAST_NUM + 1)) - fi + $lastTestingTag = git tag -l "v$baseVersion-testing.*" --sort=-version:refname | Select-Object -First 1 + + if ([string]::IsNullOrEmpty($lastTestingTag)) { + $prereleaseNum = 0 + } + elseif ($lastTestingTag -match 'testing\.(\d+)') { + $prereleaseNum = [int]$matches[1] + 1 + } + else { + Write-Host "โš ๏ธ Warning: Could not parse testing tag '$lastTestingTag', starting from 0" + $prereleaseNum = 0 + } - PRERELEASE_NUM=$(printf "%02d" $PRERELEASE_NUM) - NEW_VERSION="${BASE_VERSION}.${PRERELEASE_NUM}" - DISPLAY_VERSION="${BASE_VERSION}-testing.${PRERELEASE_NUM}" - IS_PRERELEASE="true" - TAG_NAME="v${DISPLAY_VERSION}" - RELEASE_NAME="v${DISPLAY_VERSION} ๐Ÿงช Testing Release" - RELEASE_TYPE="testing" - echo "๐Ÿงช Testing version: $DISPLAY_VERSION" - elif [[ "$BRANCH_NAME" == "dev" ]]; then + $prereleaseNum = $prereleaseNum.ToString("00") + $newVersion = "$baseVersion.$prereleaseNum" + $displayVersion = "$baseVersion-testing.$prereleaseNum" + $isPrerelease = "true" + $tagName = "v$displayVersion" + $releaseName = "v$displayVersion ๐Ÿงช Testing Release" + $releaseType = "testing" + Write-Host "๐Ÿงช Testing version: $displayVersion" + } + elseif ($branchName -eq "dev") { # Dev branch: find the next dev prerelease number - LAST_DEV_TAG=$(git tag -l "v${BASE_VERSION}-dev.*" --sort=-version:refname | head -n 1) - if [ -z "$LAST_DEV_TAG" ]; then - PRERELEASE_NUM=0 - else - LAST_NUM=$(echo "$LAST_DEV_TAG" | sed -n 's/.*-dev\.\([0-9]*\).*/\1/p') - PRERELEASE_NUM=$((LAST_NUM + 1)) - fi + $lastDevTag = git tag -l "v$baseVersion-dev.*" --sort=-version:refname | Select-Object -First 1 - PRERELEASE_NUM=$(printf "%02d" $PRERELEASE_NUM) - NEW_VERSION="${BASE_VERSION}.${PRERELEASE_NUM}" - DISPLAY_VERSION="${BASE_VERSION}-dev.${PRERELEASE_NUM}" - IS_PRERELEASE="true" - TAG_NAME="v${DISPLAY_VERSION}" - RELEASE_NAME="v${DISPLAY_VERSION} ๐Ÿ”ง Dev Release" - RELEASE_TYPE="dev" - echo "๐Ÿ”ง Dev version: $DISPLAY_VERSION" - else + if ([string]::IsNullOrEmpty($lastDevTag)) { + $prereleaseNum = 0 + } + elseif ($lastDevTag -match 'dev\.(\d+)') { + $prereleaseNum = [int]$matches[1] + 1 + } + else { + Write-Host "โš ๏ธ Warning: Could not parse dev tag '$lastDevTag', starting from 0" + $prereleaseNum = 0 + } + + $prereleaseNum = $prereleaseNum.ToString("00") + $newVersion = "$baseVersion.$prereleaseNum" + $displayVersion = "$baseVersion-dev.$prereleaseNum" + $isPrerelease = "true" + $tagName = "v$displayVersion" + $releaseName = "v$displayVersion ๐Ÿ”ง Dev Release" + $releaseType = "dev" + Write-Host "๐Ÿ”ง Dev version: $displayVersion" + } + else { # Main branch: production release - NEW_VERSION="${BASE_VERSION}.0" - DISPLAY_VERSION="${BASE_VERSION}" - IS_PRERELEASE="false" - TAG_NAME="v${DISPLAY_VERSION}" - RELEASE_NAME="v${DISPLAY_VERSION} Release" - RELEASE_TYPE="production" - echo "๐Ÿ“ฆ Production version: $DISPLAY_VERSION (${BUMP_TYPE} bump)" - fi + $newVersion = "$baseVersion.0" + $displayVersion = $baseVersion + $isPrerelease = "false" + $tagName = "v$displayVersion" + $releaseName = "v$displayVersion Release" + $releaseType = "production" + Write-Host "๐Ÿ“ฆ Production version: $displayVersion ($bumpType bump)" + } - # Update version in vsixmanifest - sed -i "s/Version=\"[^\"]*\"/Version=\"${NEW_VERSION}\"/" "$MANIFEST_FILE" + Write-Host "New version will be: $newVersion (display: $displayVersion)" - echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "DISPLAY_VERSION=$DISPLAY_VERSION" >> $GITHUB_OUTPUT - echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT - echo "BUMP_TYPE=$BUMP_TYPE" >> $GITHUB_OUTPUT - echo "IS_PRERELEASE=$IS_PRERELEASE" >> $GITHUB_OUTPUT - echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_OUTPUT - echo "RELEASE_TYPE=$RELEASE_TYPE" >> $GITHUB_OUTPUT - + # Update version in XML using proper XML manipulation + $manifest.PackageManifest.Metadata.Identity.Version = $newVersion + $manifest.Save((Resolve-Path $manifestFile)) + + # Verify the change + [xml]$verifyManifest = Get-Content $manifestFile + $newVersionCheck = $verifyManifest.PackageManifest.Metadata.Identity.Version + Write-Host "Verified new version in manifest: $newVersionCheck" + + if ($newVersionCheck -ne $newVersion) { + Write-Host "โŒ Version update failed!" + exit 1 + } + + # Set outputs + "VERSION=$newVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "DISPLAY_VERSION=$displayVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "TAG_NAME=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "BUMP_TYPE=$bumpType" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "IS_PRERELEASE=$isPrerelease" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "RELEASE_NAME=$releaseName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "RELEASE_TYPE=$releaseType" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + - name: Build Extension - run: msbuild InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj /p:Configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m + run: msbuild InterfaceExtractor.Extension/InterfaceExtractor.Extension.csproj /p:Configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:minimal /m - name: Locate VSIX file id: locate-vsix diff --git a/InterfaceExtractor.Extension/Commands/ExtractInterfaceCommand.cs b/InterfaceExtractor.Extension/Commands/ExtractInterfaceCommand.cs index 2067a99..2c208b5 100644 --- a/InterfaceExtractor.Extension/Commands/ExtractInterfaceCommand.cs +++ b/InterfaceExtractor.Extension/Commands/ExtractInterfaceCommand.cs @@ -98,8 +98,8 @@ private void OnBeforeQueryStatus(object sender, EventArgs e) private void Execute(object sender, EventArgs e) { - // Use JoinableTaskFactory.RunAsync for proper async execution from sync context - ThreadHelper.JoinableTaskFactory.RunAsync(async () => + // Use the package's JoinableTaskFactory for proper async execution + this.package.JoinableTaskFactory.RunAsync(async () => { try { @@ -107,7 +107,7 @@ private void Execute(object sender, EventArgs e) } catch (Exception ex) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await this.package.JoinableTaskFactory.SwitchToMainThreadAsync(); LogMessage($"Critical error: {ex.Message}"); LogMessage($"Stack trace: {ex.StackTrace}"); ShowMessage($"Error: {ex.Message}\n\nCheck the Output Window for details."); diff --git a/InterfaceExtractor.Tests/Integration/IntegrationTests.cs b/InterfaceExtractor.Tests/Integration/IntegrationTests.cs index 541e3df..d0b8f0d 100644 --- a/InterfaceExtractor.Tests/Integration/IntegrationTests.cs +++ b/InterfaceExtractor.Tests/Integration/IntegrationTests.cs @@ -204,18 +204,10 @@ public async Task CompleteFlow_ReadOnlyProperties_DetectsCorrectlyAsync() } [Fact] - public async Task CompleteFlow_AppendInterface_UpdatesClassAsync() + public void CompleteFlow_AppendInterface_UpdatesClassAsync() { // Arrange var sourceCode = TestHelpers.SampleCode.SimpleClass; - var filePath = TestHelpers.CreateTempCSharpFile(sourceCode, _tempDirectory); - - // Act - Analyze and generate - var classInfos = await _service.AnalyzeClassesAsync(filePath); - var interfaceCode = InterfaceExtractorService.GenerateInterface( - "ISimpleClass", - classInfos[0], - classInfos[0].Members); // Act - Append interface to class var updatedCode = InterfaceExtractorService.AppendInterfaceToClass(