diff --git a/.github/actions/setup-sql-linux/action.yml b/.github/actions/setup-sql-linux/action.yml new file mode 100644 index 00000000..1def1b13 --- /dev/null +++ b/.github/actions/setup-sql-linux/action.yml @@ -0,0 +1,51 @@ +name: 'Set up SQL Server on Linux' +description: 'Starts a SQL Server 2022 Docker container with SA auth and exports BASE_CS.' + +inputs: + sa-password: + description: 'SA password to use for the SQL Server instance.' + required: true + image: + description: 'SQL Server Docker image to use.' + required: false + default: 'mcr.microsoft.com/mssql/server:2025-latest' + +runs: + using: composite + steps: + - name: Start SQL Server container + shell: bash + env: + SA_PASSWORD: ${{ inputs.sa-password }} + MSSQL_IMAGE: ${{ inputs.image }} + run: | + docker run -d --name sqlserver \ + -e "ACCEPT_EULA=Y" \ + -e "MSSQL_SA_PASSWORD=${SA_PASSWORD}" \ + -p 1433:1433 \ + "$MSSQL_IMAGE" + + - name: Wait for SQL Server to be ready + shell: bash + env: + SA_PASSWORD: ${{ inputs.sa-password }} + run: | + for i in $(seq 1 30); do + if docker exec sqlserver /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P "${SA_PASSWORD}" -C -Q 'SELECT 1' >/dev/null 2>&1; then + echo "SQL Server is ready" + exit 0 + fi + echo "Waiting for SQL Server... ($i/30)" + sleep 5 + done + echo "SQL Server did not become ready in time" + docker logs sqlserver + exit 1 + + - name: Set connection string + shell: bash + env: + SA_PASSWORD: ${{ inputs.sa-password }} + run: | + echo "BASE_CS=Server=localhost;User ID=sa;Password=${SA_PASSWORD};TrustServerCertificate=True;" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-sql-windows/action.yml b/.github/actions/setup-sql-windows/action.yml new file mode 100644 index 00000000..c551e4ed --- /dev/null +++ b/.github/actions/setup-sql-windows/action.yml @@ -0,0 +1,93 @@ +name: 'Set up SQL Server on Windows' +description: 'Installs SQL Server 2025 Express with SA auth and exports BASE_CS.' + +inputs: + sa-password: + description: 'SA password to use for the SQL Server instance.' + required: true + +runs: + using: composite + steps: + - name: Download SQL Server 2025 Express installer + shell: pwsh + run: | + $ssei = Join-Path $env:RUNNER_TEMP 'SQL2025-SSEI-Expr.exe' + $media = Join-Path $env:RUNNER_TEMP 'sql-media' + Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/?linkid=2216019' -OutFile $ssei + Write-Host "Downloaded SSEI: $((Get-Item $ssei).Length) bytes" + + $p = Start-Process -FilePath $ssei -Wait -PassThru -ArgumentList @( + '/Quiet', + '/Action=Download', + '/Language=en-US', + "/MediaPath=$media", + '/MediaType=Core', + '/HideProgressBar' + ) + if ($p.ExitCode -ne 0) { throw "SSEI download failed with exit code $($p.ExitCode)" } + + Write-Host "=== Media directory contents ===" + Get-ChildItem $media + + - name: Install SQL Server Express with SA auth + shell: pwsh + env: + SA_PASSWORD: ${{ inputs.sa-password }} + run: | + $media = Join-Path $env:RUNNER_TEMP 'sql-media' + $selfExtract = Get-ChildItem $media -Filter 'SQLEXPR*.exe' | Select-Object -First 1 + if (-not $selfExtract) { throw "Installer EXE not found in $media" } + + $extracted = Join-Path $env:RUNNER_TEMP 'sql-extracted' + Write-Host "Extracting $($selfExtract.FullName) -> $extracted" + $extract = Start-Process -FilePath $selfExtract.FullName -Wait -PassThru ` + -ArgumentList '/Q', "/X:$extracted" + if ($extract.ExitCode -ne 0) { throw "Extraction failed with exit code $($extract.ExitCode)" } + + $setup = Join-Path $extracted 'setup.exe' + if (-not (Test-Path $setup)) { throw "setup.exe not found at $setup" } + + Write-Host "Running $setup with SECURITYMODE=SQL" + $install = Start-Process -FilePath $setup -Wait -PassThru -ArgumentList @( + '/Q', + '/ACTION=Install', + '/FEATURES=SQLEngine', + '/INSTANCENAME=MSSQLSERVER', + '/SECURITYMODE=SQL', + "/SAPWD=$env:SA_PASSWORD", + '/TCPENABLED=1', + '/IACCEPTSQLSERVERLICENSETERMS', + '/UPDATEENABLED=False', + '/SQLSYSADMINACCOUNTS=BUILTIN\Administrators' + ) + + if ($install.ExitCode -ne 0) { + Write-Host "Setup failed with exit code $($install.ExitCode)" + $logRoot = 'C:\Program Files\Microsoft SQL Server' + if (Test-Path $logRoot) { + Get-ChildItem $logRoot -Recurse -Filter 'Summary*.txt' -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 | + ForEach-Object { Write-Host "=== $($_.FullName) ==="; Get-Content $_.FullName } + } + throw "SQL Server install failed" + } + Get-Service | Where-Object { $_.Name -like 'MSSQL*' } | Format-Table + + - name: Verify SQL auth + shell: pwsh + env: + SA_PASSWORD: ${{ inputs.sa-password }} + run: | + $sqlcmd = (Get-ChildItem 'C:\Program Files\Microsoft SQL Server' -Recurse -Filter sqlcmd.exe -ErrorAction SilentlyContinue | + Select-Object -First 1).FullName + if (-not $sqlcmd) { throw "sqlcmd.exe not found after install" } + & $sqlcmd -S 'localhost' -U sa -P $env:SA_PASSWORD -b -Q "SELECT @@VERSION" + if ($LASTEXITCODE -ne 0) { throw "SQL auth verification failed" } + + - name: Set connection string + shell: bash + env: + SA_PASSWORD: ${{ inputs.sa-password }} + run: | + echo "BASE_CS=Server=localhost;User ID=sa;Password=${SA_PASSWORD};TrustServerCertificate=True;" >> "$GITHUB_ENV" diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index caa9da2b..db2c3495 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -1,162 +1,105 @@ name: pr-check -# Note: If you need to make changes to this file, please use a branch off the main branch instead of a fork. -# The pull_request target from a forked repo will not have access to the secrets needed for this workflow. +# Tests PR code against a local SQL Server instance so no Azure credentials are required. +# This workflow uses the pull_request trigger (not pull_request_target), so fork PRs run +# with no secrets and no elevated permissions. +# +# - Linux runners: spin up SQL Server 2022 in a Docker container with SA auth. +# - Windows runners: install SQL Server 2025 Express directly from Microsoft with SA auth. on: - pull_request_target: pull_request: - paths: - - '.github/workflows/pr-check.yml' permissions: {} jobs: - # Build job that safely builds artifacts from PR code without access to secrets - build: - environment: Automation test # Require approval before running the action + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} permissions: contents: read - strategy: - matrix: - os: [windows-latest, ubuntu-latest] + checks: write + + env: + TEST_DB: SqlActionTest + + defaults: + run: + shell: bash + steps: - - name: Checkout from PR branch + - name: Checkout PR uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} - - name: Verify package-lock.json exists + - name: Generate SA password run: | - if (!(Test-Path package-lock.json)) { - Write-Error "package-lock.json not found. Please commit package-lock.json to ensure reproducible builds." - exit 1 - } - shell: pwsh + SA_PASSWORD="$(openssl rand -base64 18 | tr -d '/+=')Aa1!" + echo "::add-mask::${SA_PASSWORD}" + echo "SA_PASSWORD=${SA_PASSWORD}" >> "$GITHUB_ENV" - - name: Check if package-lock.json was modified - run: | - # Check git log to see if package-lock.json was modified in this PR - git fetch origin ${{ github.base_ref }} --depth=1 - $changedFiles = git diff --name-only origin/${{ github.base_ref }}...HEAD - - if ($changedFiles -match "package-lock.json") { - Write-Warning "⚠️ package-lock.json has been modified in this PR." - Write-Warning "This requires manual review to ensure no malicious dependencies were added." - Write-Warning "Reviewers: Please carefully examine the dependency changes before approving." - } else { - Write-Host "✓ package-lock.json unchanged - no new dependencies" -ForegroundColor Green - } - shell: pwsh - continue-on-error: true - - - name: Verify package.json integrity - run: | - # Check for suspicious scripts that could be used for attacks - $packageJson = Get-Content package.json | ConvertFrom-Json - $suspiciousScripts = @('preinstall', 'postinstall', 'prepack', 'postpack') - - foreach ($script in $suspiciousScripts) { - if ($packageJson.scripts.$script) { - Write-Warning "⚠️ Found lifecycle script '$script' in package.json" - Write-Warning "Script content: $($packageJson.scripts.$script)" - Write-Warning "Reviewers: Please verify this script is legitimate" - } - } - shell: pwsh - - - name: Installing node_modules with ci (uses lockfile, ignores scripts) - run: npm ci --ignore-scripts - - - name: Audit dependencies for known vulnerabilities - run: npm audit --audit-level=high - continue-on-error: true - - - name: Build GitHub Action - run: npm run build - - - name: Upload build artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + - name: Set up SQL Server (Linux) + if: runner.os == 'Linux' + uses: ./.github/actions/setup-sql-linux with: - name: action-build-${{ matrix.os }} - path: | - lib/ - node_modules/ - action.yml - package.json - package-lock.json - retention-days: 1 - - # Deploy job that uses the built artifacts and has access to secrets - deploy: - needs: build - environment: Automation test # this environment requires approval before running the action - runs-on: ${{ matrix.os }} - permissions: - checks: write - id-token: write # This is needed for Azure login with OIDC - continue-on-error: true - strategy: - matrix: - os: [windows-latest, ubuntu-latest] - - env: - TEST_DB: 'SqlActionTest-${{ matrix.os }}' - - steps: - - name: Checkout base repository (for test data only) - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + sa-password: ${{ env.SA_PASSWORD }} - - name: Download build artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + - name: Set up SQL Server (Windows) + if: runner.os == 'Windows' + uses: ./.github/actions/setup-sql-windows with: - name: action-build-${{ matrix.os }} - path: . + sa-password: ${{ env.SA_PASSWORD }} + + - name: Build GitHub Action + run: npm ci --ignore-scripts && npm run build - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' - - name: Install SqlPackage (Linux only) - if: runner.os == 'Linux' - run: dotnet tool install -g microsoft.sqlpackage + dotnet-version: '10.x' - - name: Azure Login - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Install SqlPackage + run: dotnet tool install -g microsoft.sqlpackage - # Deploy a DACPAC with only a table to server + # Deploy a DACPAC with only a table to server (sqlpackage creates the DB if needed) - name: Test DACPAC Action uses: ./ with: - connection-string: 'Server=${{ secrets.TEST_SERVER }};Initial Catalog=${{ env.TEST_DB }};Authentication=Active Directory Default;' + connection-string: '${{ env.BASE_CS }}Initial Catalog=${{ env.TEST_DB }};' path: ./__testdata__/sql-action.dacpac action: 'publish' + skip-firewall-check: true # Build and publish sqlproj that should create a new view - name: Test Build and Publish uses: ./ with: - connection-string: 'Server=${{ secrets.TEST_SERVER }};Initial Catalog=${{ env.TEST_DB }};Authentication=Active Directory Default;' + connection-string: '${{ env.BASE_CS }}Initial Catalog=${{ env.TEST_DB }};' path: ./__testdata__/TestProject/sql-action.sqlproj action: 'publish' + skip-firewall-check: true # Execute testsql.sql via script action on server - name: Test SQL Action uses: ./ with: - connection-string: 'Server=${{ secrets.TEST_SERVER }};Initial Catalog=${{ env.TEST_DB }};Authentication=Active Directory Default;' + connection-string: '${{ env.BASE_CS }}Initial Catalog=${{ env.TEST_DB }};' path: ./__testdata__/testsql.sql + skip-firewall-check: true - name: Cleanup Test Database if: always() uses: ./ - with: - connection-string: 'Server=${{ secrets.TEST_SERVER }};Initial Catalog=master;Authentication=Active Directory Default;' + with: + connection-string: '${{ env.BASE_CS }}Initial Catalog=master;' path: ./__testdata__/cleanup.sql arguments: '-v DbName="${{ env.TEST_DB }}"' + skip-firewall-check: true + + - name: Stop SQL Server container (Linux) + if: always() && runner.os == 'Linux' + run: docker rm -f sqlserver || true