Skip to content
51 changes: 51 additions & 0 deletions .github/actions/setup-sql-linux/action.yml
Original file line number Diff line number Diff line change
@@ -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"
93 changes: 93 additions & 0 deletions .github/actions/setup-sql-windows/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: 'Set up SQL Server on Windows'
Comment thread
llali marked this conversation as resolved.
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"
163 changes: 53 additions & 110 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
@@ -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
Loading