Skip to content

Commit b2c3216

Browse files
committed
feat: add cumulative update installation for supported versions
1 parent 200d7c9 commit b2c3216

File tree

11 files changed

+2178
-2618
lines changed

11 files changed

+2178
-2618
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ jobs:
147147
sqlserver-version: ${{ matrix.sqlserver }}
148148
native-client-version: 11
149149
odbc-version: 17
150+
install-updates: true
150151
release:
151152
name: Release
152153
concurrency: release

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ See [action.yml](./action.yml):
3939
# action. A maximum of 10 attempts is made.
4040
# Default: true
4141
wait-for-ready: true
42+
43+
# Attempt to install latest cumulative updates during the installation process
44+
# (not available for all versions).
45+
# Default: false
46+
install-updates: false
4247
```
4348
<!-- end usage -->
4449

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ inputs:
2323
wait-for-ready:
2424
description: 'Wait for the database to respond successfully to queries before completing the action. A maximum of 10 attempts is made.'
2525
default: 'true'
26+
install-updates:
27+
description: 'Attempt to install latest cumulative updates during the installation process (not available for all versions).'
28+
default: 'false'
2629
outputs:
2730
sa-password:
2831
description: 'The SA password, this will be the same as the input, but can be useful when relying on the default value.'

lib/main/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 1940 additions & 2612 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@actions/core": "^1.10.0",
5858
"@actions/exec": "^1.1.1",
5959
"@actions/glob": "^0.4.0",
60+
"@actions/http-client": "^2.2.1",
6061
"@actions/io": "^1.1.3",
6162
"@actions/tool-cache": "^2.0.1"
6263
}

src/install.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os from 'os';
2-
import { basename, join as joinPaths } from 'path';
2+
import { basename, dirname, join as joinPaths } from 'path';
33
import { readFile } from 'fs/promises';
44
import * as core from '@actions/core';
55
import * as exec from '@actions/exec';
@@ -8,6 +8,7 @@ import { VersionConfig, VERSIONS } from './versions';
88
import {
99
downloadBoxInstaller,
1010
downloadExeInstaller,
11+
downloadUpdateInstaller,
1112
gatherInputs,
1213
gatherSummaryFiles,
1314
getOsVersion,
@@ -33,6 +34,15 @@ function findOrDownloadTool(config: VersionConfig): Promise<string> {
3334
return downloadExeInstaller(config);
3435
}
3536

37+
function findOrDownloadUpdates(config: VersionConfig): Promise<string> {
38+
const toolPath = tc.find('sqlupdate', config.version);
39+
if (toolPath) {
40+
core.info(`Found in cache @ ${toolPath}`);
41+
return Promise.resolve(joinPaths(toolPath, 'sqlupdate.exe'));
42+
}
43+
return downloadUpdateInstaller(config);
44+
}
45+
3646
export default async function install() {
3747
let threw = false;
3848
const {
@@ -44,6 +54,7 @@ export default async function install() {
4454
skipOsCheck,
4555
nativeClientVersion,
4656
odbcVersion,
57+
installUpdates,
4758
} = gatherInputs();
4859
// we only support windows for now. But allow crazy people to skip this check if they like...
4960
if (!skipOsCheck && os.platform() !== 'win32') {
@@ -86,6 +97,16 @@ export default async function install() {
8697
}
8798
// Initial checks complete - fetch the installer
8899
const toolPath = await core.group(`Fetching install media for ${version}`, () => findOrDownloadTool(config));
100+
if (installUpdates) {
101+
if (!config.updateUrl) {
102+
core.info('Skipping update installation - version not supported');
103+
} else {
104+
const updatePath = await core.group(`Fetching cumulative updates for ${version}`, () => findOrDownloadUpdates(config));
105+
if (updatePath) {
106+
installArgs.push('/UPDATEENABLED=1', `/UpdateSource=${dirname(updatePath)}`);
107+
}
108+
}
109+
}
89110
const instanceName = 'MSSQLSERVER';
90111
try {
91112
// @todo - make sure that the arguments are unique / don't conflict

src/utils.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as exec from '@actions/exec';
22
import * as core from '@actions/core';
33
import * as tc from '@actions/tool-cache';
44
import * as io from '@actions/io';
5+
import * as http from '@actions/http-client';
56
import { basename, extname, dirname, join as joinPaths } from 'path';
67
import { VersionConfig } from './versions';
78
import { generateFileHash } from './crypto';
@@ -50,6 +51,7 @@ export interface Inputs {
5051
skipOsCheck: boolean;
5152
nativeClientVersion: string;
5253
odbcVersion: string;
54+
installUpdates: boolean;
5355
}
5456

5557
/**
@@ -68,6 +70,7 @@ export function gatherInputs(): Inputs {
6870
skipOsCheck: core.getBooleanInput('skip-os-check'),
6971
nativeClientVersion: core.getInput('native-client-version'),
7072
odbcVersion: core.getInput('odbc-version'),
73+
installUpdates: core.getBooleanInput('install-updates'),
7174
};
7275
}
7376

@@ -170,7 +173,44 @@ export async function downloadExeInstaller(config: VersionConfig): Promise<strin
170173
return joinPaths(toolPath, 'setup.exe');
171174
}
172175

173-
176+
/**
177+
* Downloads cumulative updates for supported versions.
178+
*
179+
* @param {VersionConfig} config
180+
* @returns {Promise<string>}
181+
*/
182+
export async function downloadUpdateInstaller(config: VersionConfig): Promise<string> {
183+
if (!config.updateUrl) {
184+
throw new Error('No update url provided');
185+
}
186+
// resolve download url
187+
let downloadLink: string | null = null;
188+
if (!config.updateUrl.endsWith('.exe')) {
189+
const client = new http.HttpClient();
190+
const res = await client.get(config.updateUrl);
191+
if (res.message.statusCode && res.message.statusCode >= 200 && res.message.statusCode < 300) {
192+
const body = await res.readBody();
193+
const [, link] = body.match(/\s+href\s*=\s*["'](https:\/\/download\.microsoft\.com\/.*\.exe)['"]/) ?? [];
194+
if (link) {
195+
downloadLink = link;
196+
}
197+
}
198+
if (!downloadLink) {
199+
core.warning('Unable to download cumulative updates');
200+
return '';
201+
}
202+
}
203+
core.info(`Downloading cumulative update from ${downloadLink ?? config.updateUrl}`);
204+
const updatePath = await downloadTool(downloadLink ?? config.updateUrl);
205+
if (core.isDebug()) {
206+
const hash = await generateFileHash(updatePath);
207+
core.debug(`Got update file with hash SHA256=${hash.toString('base64')}`);
208+
}
209+
core.info('Adding to the cache');
210+
const toolPath = await tc.cacheFile(updatePath, 'sqlupdate.exe', 'sqlupdate', config.version);
211+
core.debug(`Cached @ ${toolPath}`);
212+
return joinPaths(toolPath, 'sqlupdate.exe');
213+
}
174214

175215
/**
176216
* Gather installation summary file. Used after installation to output summary data.

src/versions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface Config {
1212
export interface VersionConfig extends Config {
1313
exeUrl: string;
1414
boxUrl?: string;
15+
updateUrl?: string;
1516
}
1617

1718
export const VERSIONS = new Map<string, VersionConfig>(
@@ -20,21 +21,25 @@ export const VERSIONS = new Map<string, VersionConfig>(
2021
version: '2022',
2122
exeUrl: 'https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLServer2022-DEV-x64-ENU.exe',
2223
boxUrl: 'https://download.microsoft.com/download/3/8/d/38de7036-2433-4207-8eae-06e247e17b25/SQLServer2022-DEV-x64-ENU.box',
24+
updateUrl: 'https://www.microsoft.com/en-us/download/details.aspx?id=105013',
2325
}],
2426
['2019', {
2527
version: '2019',
2628
exeUrl: 'https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLServer2019-DEV-x64-ENU.exe',
2729
boxUrl: 'https://download.microsoft.com/download/8/4/c/84c6c430-e0f5-476d-bf43-eaaa222a72e0/SQLServer2019-DEV-x64-ENU.box',
30+
updateUrl: 'https://www.microsoft.com/en-us/download/details.aspx?id=100809',
2831
}],
2932
['2017', {
3033
version: '2017',
3134
exeUrl: 'https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLServer2017-DEV-x64-ENU.exe',
3235
boxUrl: 'https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLServer2017-DEV-x64-ENU.box',
36+
updateUrl: 'https://www.microsoft.com/en-us/download/details.aspx?id=56128',
3337
}],
3438
['2016', {
3539
version: '2016',
3640
exeUrl: 'https://download.microsoft.com/download/4/1/A/41AD6EDE-9794-44E3-B3D5-A1AF62CD7A6F/sql16_sp2_dlc/en-us/SQLServer2016SP2-FullSlipstream-DEV-x64-ENU.exe',
3741
boxUrl: 'https://download.microsoft.com/download/4/1/A/41AD6EDE-9794-44E3-B3D5-A1AF62CD7A6F/sql16_sp2_dlc/en-us/SQLServer2016SP2-FullSlipstream-DEV-x64-ENU.box',
42+
updateUrl: 'https://download.microsoft.com/download/a/7/7/a77b5753-8fe7-4804-bfc5-591d9a626c98/SQLServer2016SP3-KB5003279-x64-ENU.exe',
3843
}],
3944
['2014', {
4045
osSupport: {

test/install.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ describe('install', () => {
3535
versionStub.get.withArgs('box').returns({
3636
version: '2022',
3737
exeUrl: 'https://example.com/installer.exe',
38-
boxUrl: 'https://example.om/installer.box',
38+
boxUrl: 'https://example.com/installer.box',
39+
updateUrl: 'https://example.com/update.html',
3940
});
4041
versionStub.get.withArgs('exe').returns({
4142
version: '2019',
4243
exeUrl: 'https://example.com/setup.exe',
44+
updateUrl: 'https://example.com/update.exe',
4345
});
4446
versionStub.get.withArgs('maxOs').returns({
4547
version: '2017',
@@ -73,11 +75,13 @@ describe('install', () => {
7375
skipOsCheck: false,
7476
nativeClientVersion: '',
7577
odbcVersion: '',
78+
installUpdates: false,
7679
});
7780
utilsStub.getOsVersion.resolves(2022);
7881
utilsStub.gatherSummaryFiles.resolves([]);
7982
utilsStub.downloadExeInstaller.resolves('C:/tmp/exe/setup.exe');
8083
utilsStub.downloadBoxInstaller.resolves('C:/tmp/box/setup.exe');
84+
utilsStub.downloadUpdateInstaller.resolves('C:/tmp/exe/sqlupdate.exe');
8185
utilsStub.waitForDatabase.resolves(0);
8286
osStub = stub(os);
8387
osStub.platform.returns('win32');
@@ -115,6 +119,7 @@ describe('install', () => {
115119
skipOsCheck: false,
116120
nativeClientVersion: '',
117121
odbcVersion: '',
122+
installUpdates: false,
118123
});
119124
try {
120125
await install();
@@ -138,12 +143,71 @@ describe('install', () => {
138143
skipOsCheck: false,
139144
nativeClientVersion: '',
140145
odbcVersion: '',
146+
installUpdates: false,
141147
});
142148
await install();
143149
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
144150
});
151+
it('downloads cumulative updates', async () => {
152+
utilsStub.gatherInputs.returns({
153+
version: 'exe',
154+
password: 'secret password',
155+
collation: 'SQL_Latin1_General_CP1_CI_AS',
156+
installArgs: [],
157+
wait: true,
158+
skipOsCheck: false,
159+
nativeClientVersion: '',
160+
odbcVersion: '',
161+
installUpdates: true,
162+
});
163+
await install();
164+
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
165+
expect(execStub.exec.firstCall.args[1]).to.contain.members([
166+
'/UPDATEENABLED=1',
167+
'/UpdateSource=C:/tmp/exe',
168+
]);
169+
});
170+
it('uses cached updates if found', async () => {
171+
tcStub.find.withArgs('sqlupdate').returns('C:/tool-cache/sql-update');
172+
utilsStub.gatherInputs.returns({
173+
version: 'exe',
174+
password: 'secret password',
175+
collation: 'SQL_Latin1_General_CP1_CI_AS',
176+
installArgs: [],
177+
wait: true,
178+
skipOsCheck: false,
179+
nativeClientVersion: '',
180+
odbcVersion: '',
181+
installUpdates: true,
182+
});
183+
await install();
184+
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
185+
expect(execStub.exec.firstCall.args[1]).to.contain.members([
186+
'/UPDATEENABLED=1',
187+
'/UpdateSource=C:/tool-cache/sql-update',
188+
]);
189+
});
190+
it('skips cumulative updates if no update url', async () => {
191+
utilsStub.gatherInputs.returns({
192+
version: 'minOs',
193+
password: 'secret password',
194+
collation: 'SQL_Latin1_General_CP1_CI_AS',
195+
installArgs: [],
196+
wait: true,
197+
skipOsCheck: false,
198+
nativeClientVersion: '',
199+
odbcVersion: '',
200+
installUpdates: true,
201+
});
202+
await install();
203+
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
204+
expect(execStub.exec.firstCall.args[1]).to.not.contain.members([
205+
'/UPDATEENABLED=1',
206+
'/UpdateSource=C:/tmp/exe',
207+
]);
208+
});
145209
it('uses cached tool if found', async () => {
146-
tcStub.find.returns('C:/tool-cache/sql-server');
210+
tcStub.find.withArgs('sqlserver').returns('C:/tool-cache/sql-server');
147211
await install();
148212
expect(execStub.exec).to.have.been.calledWith('"C:/tool-cache/sql-server/setup.exe"', match.array, { windowsVerbatimArguments: true });
149213
});
@@ -157,6 +221,7 @@ describe('install', () => {
157221
skipOsCheck: false,
158222
nativeClientVersion: '',
159223
odbcVersion: '',
224+
installUpdates: false,
160225
});
161226
try {
162227
await install();
@@ -177,6 +242,7 @@ describe('install', () => {
177242
skipOsCheck: false,
178243
nativeClientVersion: '',
179244
odbcVersion: '',
245+
installUpdates: false,
180246
});
181247
try {
182248
await install();
@@ -197,6 +263,7 @@ describe('install', () => {
197263
skipOsCheck: false,
198264
nativeClientVersion: '',
199265
odbcVersion: '',
266+
installUpdates: false,
200267
});
201268
try {
202269
await install();
@@ -217,6 +284,7 @@ describe('install', () => {
217284
skipOsCheck: false,
218285
nativeClientVersion: '',
219286
odbcVersion: '',
287+
installUpdates: false,
220288
});
221289
await install();
222290
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
@@ -232,6 +300,7 @@ describe('install', () => {
232300
skipOsCheck: false,
233301
nativeClientVersion: '',
234302
odbcVersion: '',
303+
installUpdates: false,
235304
});
236305
await install();
237306
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
@@ -246,6 +315,7 @@ describe('install', () => {
246315
skipOsCheck: true,
247316
nativeClientVersion: '',
248317
odbcVersion: '',
318+
installUpdates: false,
249319
});
250320
await install();
251321
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
@@ -282,6 +352,7 @@ describe('install', () => {
282352
skipOsCheck: false,
283353
nativeClientVersion: '',
284354
odbcVersion: '',
355+
installUpdates: false,
285356
});
286357
const stubReadfile = stub(fs, 'readFile');
287358
stubReadfile.resolves(Buffer.from('test data'));
@@ -327,6 +398,7 @@ describe('install', () => {
327398
skipOsCheck: false,
328399
nativeClientVersion: '11',
329400
odbcVersion: '',
401+
installUpdates: false,
330402
});
331403
await install();
332404
expect(stubNc.default).to.have.been.calledOnceWith('11');
@@ -341,6 +413,7 @@ describe('install', () => {
341413
skipOsCheck: false,
342414
nativeClientVersion: '11',
343415
odbcVersion: '18',
416+
installUpdates: false,
344417
});
345418
await install();
346419
expect(stubNc.default).to.have.been.calledOnceWith('11');

0 commit comments

Comments
 (0)