Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The build step (`npm run build`) also runs `npm run docs`, which regenerates the

**Installer abstraction:** `src/installers/` contains a base `Installer` class and `MsiInstaller` subclass used by the native client and ODBC installations. SQL Server itself uses direct exe/box download logic or the SSEI bootstrapper (for 2025+) in `src/utils.ts`.

**Version registry:** `src/versions.ts` defines a `Map<string, VersionConfig>` with download URLs (exe/box or SSEI), optional update URLs, and OS compatibility constraints for each supported SQL Server version (2008–2025). SQL Server 2025+ uses the SSEI bootstrapper model (`sseiUrl`) instead of direct exe/box downloads.
**Version registry:** `src/versions.ts` defines a `Map<string, VersionConfig>` with download URLs (exe/box or SSEI), optional update URLs, and OS compatibility constraints for each supported SQL Server version (2008–2025). SQL Server 2025+ uses the SSEI bootstrapper model (`sseiUrl`) instead of direct exe/box downloads. For SSEI versions, `downloadSseiInstaller()` downloads the media into an isolated temporary directory before extracting and caching the installer. Box and SSEI installers are both extracted into their own temporary directory before caching.

**Build output:** Rollup (config in `rollup.config.mjs`) bundles everything into `lib/main/index.js` as a minified ESM module, which is what `action.yml` references. The `lib/` directory is committed to the repository. **Every commit must include up-to-date build output** — CI checks this by rebuilding and running `git diff-files --quiet`. Always run `npm run build` and commit the resulting changes to `lib/` and `README.md` before pushing.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
strategy:
matrix:
os: [ 'windows-2022', 'windows-2025' ]
sqlserver: [ 'sql-latest', 'sql-2022', 'sql-2019', 'sql-2017', 'sql-2016' ]
sqlserver: [ 'sql-latest', 'sql-2025', 'sql-2022', 'sql-2019', 'sql-2017', 'sql-2016' ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ See [action.yml](./action.yml):
# Default: false
skip-os-check: false

# Version to use. Examples: 2008, 2012, 2014, etc. "latest" can also be used.
# SQL Server version: 2008, 2012, 2014, 2016, 2017, 2019, 2022, or 2025. "latest"
# selects 2025.
# Default: latest
sqlserver-version: 'latest'

Expand Down Expand Up @@ -47,6 +48,20 @@ See [action.yml](./action.yml):
```
<!-- end usage -->

### SQL Server 2025

Set `sqlserver-version` to `'2025'` (or `'sql-2025'`) to install SQL Server
2025 Standard Developer edition. The default, `'latest'`, also selects 2025.
The action downloads the installation media using Microsoft's SSEI bootstrapper
and attempts to cache the extracted installer for subsequent runs.

```yaml
- name: Install SQL Server 2025
uses: tediousjs/setup-sqlserver@v4
with:
sqlserver-version: '2025'
```

### Cumulative updates

When `install-updates: true` is set for a version with a configured update URL,
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ inputs:
description: 'Skip OS checks that will stop installation attempts preemptively.'
default: 'false'
sqlserver-version:
description: 'Version to use. Examples: 2008, 2012, 2014, etc. "latest" can also be used.'
description: 'SQL Server version: 2008, 2012, 2014, 2016, 2017, 2019, 2022, or 2025. "latest" selects 2025.'
default: 'latest'
native-client-version:
description: 'Version of native client to install. Only 11 is supported.'
Expand Down
6 changes: 3 additions & 3 deletions lib/main/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/main/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default async function install() {
}
});
if (updatePath) {
installArgs.push('/UPDATEENABLED=1', `/UpdateSource=${dirname(updatePath)}`);
installArgs.push('/UPDATEENABLED=1', `/UpdateSource="${dirname(updatePath)}"`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/installers/msi-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class MsiInstaller extends Installer {
core.info('Running installer');
const args: string[] = [
'/i',
path,
`"${path}"`,
];
if (this.guid) {
args.push(`APPGUID={${this.guid}}`);
Expand Down
51 changes: 27 additions & 24 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdtemp, readdir } from 'node:fs/promises';
Comment thread
dhensby marked this conversation as resolved.
import { basename, extname, dirname, join as joinPaths } from 'node:path';
import { readdir } from 'node:fs/promises';
import { setTimeout as delay } from 'node:timers/promises';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
Expand Down Expand Up @@ -137,17 +137,30 @@ export async function downloadBoxInstaller(config: VersionConfig): Promise<strin
core.debug(`Got setup file (exe) with hash SHA256=${hashes[0].toString('base64')}`);
core.debug(`Got setup file (box) with hash SHA256=${hashes[1].toString('base64')}`);
}
const downloadDir = dirname(exePath);
core.info(`Extracting installer`);
return extractAndCacheInstaller(exePath, config.version);
}

/**
* Extract a self-extracting SQL Server installer into its own temporary
* directory and add the extracted files to the tool cache.
*
* @param {string} exePath
* @param {string} version
* @returns {Promise<string>} The path to the cached setup.exe
*/
async function extractAndCacheInstaller(exePath: string, version: string): Promise<string> {
const workDir = dirname(exePath);
const setupDir = await mkdtemp(joinPaths(workDir, 'sqlserver-setup-'));
core.info('Extracting installer');
await exec.exec(`"${exePath}"`, [
'/qs',
`/x:setup`,
`/x:"${setupDir}"`,
], {
cwd: downloadDir,
cwd: workDir,
windowsVerbatimArguments: true,
});
core.info('Adding to the cache');
const toolPath = await tc.cacheDir(joinPaths(downloadDir, 'setup'), 'sqlserver', config.version);
const toolPath = await tc.cacheDir(setupDir, 'sqlserver', version);
core.debug(`Cached @ ${toolPath}`);
return joinPaths(toolPath, 'setup.exe');
}
Expand All @@ -171,36 +184,26 @@ export async function downloadSseiInstaller(config: VersionConfig): Promise<stri
core.debug(`Got SSEI bootstrapper with hash SHA256=${hash.toString('base64')}`);
}
// use the bootstrapper to download the actual media
const mediaDir = dirname(sseiPath);
const mediaDir = await mkdtemp(joinPaths(dirname(sseiPath), 'sqlserver-media-'));
Comment thread
cheenamalhotra marked this conversation as resolved.
core.info('Downloading install media via SSEI bootstrapper');
await exec.exec(`"${sseiPath}"`, [
'/Action=Download',
`/MediaPath=${mediaDir}`,
`/MediaPath="${mediaDir}"`,
Comment thread
cheenamalhotra marked this conversation as resolved.
'/MediaType=CAB',
'/Quiet',
'/Language=en-US',
], {
windowsVerbatimArguments: true,
});
// find the downloaded exe in the media directory
const files = await readdir(mediaDir);
const exeFile = files.find((f) => f.endsWith('.exe') && f !== basename(sseiPath));
if (!exeFile) {
const exeFiles = files.filter((file) => file.toLowerCase().endsWith('.exe'));
if (exeFiles.length === 0) {
throw new Error('SSEI bootstrapper did not produce an installer exe');
}
const exePath = joinPaths(mediaDir, exeFile);
core.info('Extracting installer');
await exec.exec(`"${exePath}"`, [
'/qs',
'/x:setup',
], {
cwd: mediaDir,
windowsVerbatimArguments: true,
});
core.info('Adding to the cache');
const toolPath = await tc.cacheDir(joinPaths(mediaDir, 'setup'), 'sqlserver', config.version);
core.debug(`Cached @ ${toolPath}`);
return joinPaths(toolPath, 'setup.exe');
if (exeFiles.length > 1) {
throw new Error(`SSEI bootstrapper produced multiple installer exes: ${exeFiles.join(', ')}`);
}
return extractAndCacheInstaller(joinPaths(mediaDir, exeFiles[0]), config.version);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions test/install-native-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('install-native-client', () => {
assert.equal(call.arguments[0], 'msiexec');
assert.ok(Array.isArray(call.arguments[1]));
assert.deepEqual(call.arguments[2], { windowsVerbatimArguments: true });
assert.ok(call.arguments[1].includes('C:/tmp/sqlncli.msi'));
assert.ok(call.arguments[1].includes('"C:/tmp/sqlncli.msi"'));
});
it('installs from web (x64)', async () => {
Object.defineProperty(process, 'arch', { value: 'x64' });
Expand All @@ -62,7 +62,7 @@ describe('install-native-client', () => {
assert.equal(exec.exec.mock.callCount(), 1);
const call = exec.exec.mock.calls[0];
assert.equal(call.arguments[0], 'msiexec');
assert.ok(call.arguments[1].includes('C:/tmp/cache/sqlncli.msi'));
assert.ok(call.arguments[1].includes('"C:/tmp/cache/sqlncli.msi"'));
});
it('installs from web (x32)', async () => {
Object.defineProperty(process, 'arch', { value: 'x32' });
Expand All @@ -72,7 +72,7 @@ describe('install-native-client', () => {
assert.equal(tc.cacheFile.mock.callCount(), 1);
const call = exec.exec.mock.calls[0];
assert.equal(call.arguments[0], 'msiexec');
assert.ok(call.arguments[1].includes('C:/tmp/cache/sqlncli.msi'));
assert.ok(call.arguments[1].includes('"C:/tmp/cache/sqlncli.msi"'));
});
});
});
6 changes: 3 additions & 3 deletions test/install-odbc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('install-odbc', () => {
[
'/passive',
'/i',
'C:/tmp/msodbcsql.msi',
'"C:/tmp/msodbcsql.msi"',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
],
{ windowsVerbatimArguments: true },
Expand All @@ -69,7 +69,7 @@ describe('install-odbc', () => {
[
'/passive',
'/i',
'C:/tmp/cache/msodbcsql.msi',
'"C:/tmp/cache/msodbcsql.msi"',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
],
{ windowsVerbatimArguments: true },
Expand All @@ -86,7 +86,7 @@ describe('install-odbc', () => {
[
'/passive',
'/i',
'C:/tmp/cache/msodbcsql.msi',
'"C:/tmp/cache/msodbcsql.msi"',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
],
{ windowsVerbatimArguments: true },
Expand Down
12 changes: 10 additions & 2 deletions test/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,20 +158,28 @@ describe('install', () => {
await install();
assert.equal(exec.exec.mock.calls[0].arguments[0], '"C:/tmp/ssei/setup.exe"');
});
it('uses cached SSEI media without running the bootstrapper again', async () => {
utils.gatherInputs.mock.mockImplementation(() => defaultInputs({ version: 'ssei' }));
tc.find.mock.mockImplementation(() => 'C:/tool-cache/sqlserver/2025');
await install();
assert.deepEqual(tc.find.mock.calls[0].arguments, ['sqlserver', '2025']);
assert.equal(exec.exec.mock.calls[0].arguments[0], '"C:/tool-cache/sqlserver/2025/setup.exe"');
assert.equal(utils.downloadSseiInstaller.mock.callCount(), 0);
});
it('downloads cumulative updates', async () => {
utils.gatherInputs.mock.mockImplementation(() => defaultInputs({ version: 'exe', installUpdates: true }));
await install();
const args = exec.exec.mock.calls[0].arguments[1] as string[];
assert.ok(args.includes('/UPDATEENABLED=1'));
assert.ok(args.includes('/UpdateSource=C:/tmp/exe'));
assert.ok(args.includes('/UpdateSource="C:/tmp/exe"'));
});
it('uses cached updates if found', async () => {
tc.find.mock.mockImplementation((tool: string) => tool === 'sqlupdate' ? 'C:/tool-cache/sql-update' : '');
utils.gatherInputs.mock.mockImplementation(() => defaultInputs({ version: 'exe', installUpdates: true }));
await install();
const args = exec.exec.mock.calls[0].arguments[1] as string[];
assert.ok(args.includes('/UPDATEENABLED=1'));
assert.ok(args.includes('/UpdateSource=C:/tool-cache/sql-update'));
assert.ok(args.includes('/UpdateSource="C:/tool-cache/sql-update"'));
});
for (const error of [new Error('Unable to fetch cumulative update page: HTTP 403'), 'Unable to fetch cumulative update page: HTTP 403']) {
it(`installs without updates and warns if requested updates fail with ${typeof error}`, async () => {
Expand Down
10 changes: 5 additions & 5 deletions test/installers/msi-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('MsiInstaller', () => {
assert.equal(exec.exec.mock.callCount(), 1);
assert.deepEqual(exec.exec.mock.calls[0].arguments, [
'msiexec',
['/passive', '/i', 'C:/cache/test/x86/test.msi'],
['/passive', '/i', '"C:/cache/test/x86/test.msi"'],
{ windowsVerbatimArguments: true },
]);
});
Expand All @@ -145,7 +145,7 @@ describe('MsiInstaller', () => {
assert.equal(tc.cacheFile.mock.callCount(), 1);
assert.deepEqual(exec.exec.mock.calls[0].arguments, [
'msiexec',
['/passive', '/i', 'C:/cache/installer/test.msi'],
['/passive', '/i', '"C:/cache/installer/test.msi"'],
{ windowsVerbatimArguments: true },
]);
});
Expand All @@ -160,7 +160,7 @@ describe('MsiInstaller', () => {
await installer.install();
assert.deepEqual(exec.exec.mock.calls[0].arguments, [
'msiexec',
['/passive', '/i', 'C:/cache/installer/test.msi', `APPGUID={${appGuid}}`],
['/passive', '/i', '"C:/cache/installer/test.msi"', `APPGUID={${appGuid}}`],
{ windowsVerbatimArguments: true },
]);
});
Expand All @@ -174,7 +174,7 @@ describe('MsiInstaller', () => {
await installer.install();
assert.deepEqual(exec.exec.mock.calls[0].arguments, [
'msiexec',
['/i', 'C:/cache/installer/test.msi'],
['/i', '"C:/cache/installer/test.msi"'],
{ windowsVerbatimArguments: true },
]);
});
Expand All @@ -188,7 +188,7 @@ describe('MsiInstaller', () => {
await installer.install();
assert.deepEqual(exec.exec.mock.calls[0].arguments, [
'msiexec',
['/passive', '/i', 'C:/cache/installer/test.msi', 'REINSTALL="ALL"'],
['/passive', '/i', '"C:/cache/installer/test.msi"', 'REINSTALL="ALL"'],
{ windowsVerbatimArguments: true },
]);
});
Expand Down
Loading