Skip to content
Open
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 @@ -47,7 +47,7 @@ The build step (`npm run build`) also runs `npm run docs`, which regenerates the
3. Optionally installs SQL Native Client (`src/install-native-client.ts`) and ODBC driver (`src/install-odbc.ts`)
4. Downloads or cache-hits the SQL Server installer (box+exe, standalone exe, or SSEI bootstrapper)
5. Optionally downloads cumulative updates (resolves Microsoft download-page JSON, falls back to legacy links if metadata is unusable, retries network errors, timeouts and all non-2xx page responses up to three times with 5- and 10-second delays, and warns then installs without updates if the download fails)
6. Runs the installer via `@actions/exec`
6. Runs the installer via `@actions/exec` (exit code 3010, "succeeded but reboot required", is logged as a warning and treated as success; any other non-zero code fails the action)
7. Waits for the database to be ready (exponential backoff)

**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`.
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,15 @@ the action downloads the update before starting SQL Server setup. Download-page
requests are tried up to three times on network errors, timeouts, or non-2xx
responses (including 403 and 404), with 5- and 10-second delays before retries.
If the update still can't be downloaded, the action logs a warning with the
reason and installs SQL Server without updates. Versions without a configured
update URL skip updates.
reason and installs SQL Server without updates. Update URLs are configured for
SQL Server 2016 and later, including 2025 (and so the default, `'latest'`);
older versions skip updates.

### Reboot requests

If SQL Server setup succeeds but asks for a reboot (exit code 3010), the action
logs a warning and carries on without rebooting. The wait-for-ready check (when
`wait-for-ready` is enabled) then confirms the instance is accepting queries.

### Basic usage

Expand Down
2 changes: 1 addition & 1 deletion 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.

10 changes: 9 additions & 1 deletion src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default async function install() {
const instanceName = 'MSSQLSERVER';
try {
// @todo - make sure that the arguments are unique / don't conflict
await core.group('Installing SQL Server', () => exec.exec(`"${toolPath}"`, [
const exitCode = await core.group('Installing SQL Server', () => exec.exec(`"${toolPath}"`, [
'/q',
'/ACTION=Install',
'/FEATURES=SQLEngine', //,FullText,RS,Tools
Expand All @@ -137,7 +137,15 @@ export default async function install() {
...installArgs,
], {
windowsVerbatimArguments: true,
ignoreReturnCode: true,
}));
// 3010 means setup succeeded but requested a reboot, which isn't possible mid-job. Carry on
// and let the readiness check confirm the instance is usable.
if (exitCode === 3010) {
core.warning('SQL Server setup succeeded but requested a reboot (exit code 3010); continuing without rebooting');
} else if (exitCode !== 0) {
throw new Error(`SQL Server setup failed with exit code ${exitCode}`);
}
// set outputs
core.setOutput('sa-password', password);
core.setOutput('instance-name', instanceName);
Expand Down
2 changes: 1 addition & 1 deletion src/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const VERSIONS = new Map<string, VersionConfig>(
['2025', {
version: '2025',
sseiUrl: 'https://download.microsoft.com/download/77dc60d3-0139-4dad-83c8-bb52ab22db01/SQL2025-SSEI-StdDev.exe',
// updateUrl can be added once Microsoft publishes cumulative updates for 2025
updateUrl: 'https://www.microsoft.com/en-us/download/details.aspx?id=108788',
}],
['2022', {
version: '2022',
Expand Down
27 changes: 26 additions & 1 deletion test/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,22 @@ describe('install', () => {
await install();
const call = exec.exec.mock.calls[0];
assert.equal(call.arguments[0], '"C:/tmp/box/setup.exe"');
assert.deepEqual(call.arguments[2], { windowsVerbatimArguments: true });
assert.deepEqual(call.arguments[2], { windowsVerbatimArguments: true, ignoreReturnCode: true });
});
it('warns and waits for the database if setup requests a reboot', async () => {
exec.exec.mock.mockImplementation(async () => 3010);
await install();
assert.equal(core.warning.mock.callCount(), 1);
assert.equal(core.warning.mock.calls[0].arguments[0], 'SQL Server setup succeeded but requested a reboot (exit code 3010); continuing without rebooting');
assert.equal(utils.waitForDatabase.mock.callCount(), 1);
assert.deepEqual(core.setOutput.mock.calls.map((c) => c.arguments), [['sa-password', 'secret password'], ['instance-name', 'MSSQLSERVER']]);
});
it('fails if setup exits with any other non-zero code', async () => {
exec.exec.mock.mockImplementation(async () => 1);
await assert.rejects(() => install(), { message: 'SQL Server setup failed with exit code 1' });
assert.equal(utils.waitForDatabase.mock.callCount(), 0);
assert.equal(core.setOutput.mock.callCount(), 0);
assert.equal(utils.gatherSummaryFiles.mock.calls[0].arguments[0], true);
});
it('runs an exe install', async () => {
utils.gatherInputs.mock.mockImplementation(() => defaultInputs({ version: 'exe' }));
Expand All @@ -173,6 +188,16 @@ describe('install', () => {
assert.ok(args.includes('/UPDATEENABLED=1'));
assert.ok(args.includes('/UpdateSource="C:/tmp/exe"'));
});
it('downloads cumulative updates for an ssei install', async () => {
utils.gatherInputs.mock.mockImplementation(() => defaultInputs({ version: 'ssei', installUpdates: true }));
await install();
assert.equal(utils.downloadUpdateInstaller.mock.calls[0].arguments[0], VERSIONS.get('ssei'));
const call = exec.exec.mock.calls[0];
assert.equal(call.arguments[0], '"C:/tmp/ssei/setup.exe"');
const args = call.arguments[1] as string[];
assert.ok(args.includes('/UPDATEENABLED=1'));
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 }));
Expand Down
5 changes: 5 additions & 0 deletions test/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ describe('versions', () => {
'2008',
]);
});
for (const version of ['2025', '2022', '2019', '2017', '2016']) {
it(`configures cumulative updates for ${version}`, () => {
assert.match(versions.VERSIONS.get(version)?.updateUrl ?? '', /^https:\/\/(www|download)\.microsoft\.com\//);
});
}
});
});
Loading