Skip to content

Commit 27e800f

Browse files
committed
refactor: create a generic MSI Installer class
1 parent b9278e9 commit 27e800f

File tree

13 files changed

+464
-49
lines changed

13 files changed

+464
-49
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ See [action.yml](./action.yml):
1717
# Default: latest
1818
sqlserver-version: ''
1919

20-
# Version of native client to installer. Only 11 is supported.
20+
# Version of native client to install. Only 11 is supported.
2121
native-client-version: ''
2222

2323
# The SA user password to use.

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ inputs:
99
description: 'Version to use. Examples: 2008, 2012, 2014, etc. "latest" can also be used.'
1010
default: 'latest'
1111
native-client-version:
12-
description: 'Version of native client to installer. Only 11 is supported.'
12+
description: 'Version of native client to install. Only 11 is supported.'
1313
sa-password:
1414
description: 'The SA user password to use.'
1515
default: 'yourStrong(!)Password'

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.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "eslint --config ./.eslintrc.json ./src ./misc ./test",
1111
"lint:fix": "npm run lint -- --fix",
1212
"prepare": "npm run build",
13-
"test": "mocha -r ts-node/register './test/**.ts'",
13+
"test": "mocha -r ts-node/register './test/**/**.ts'",
1414
"test:coverage": "nyc --all npm run test --silent"
1515
},
1616
"files": [

src/install-native-client.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,24 @@
1-
import * as core from '@actions/core';
2-
import * as tc from '@actions/tool-cache';
3-
import * as exec from '@actions/exec';
4-
import { downloadTool } from './utils';
5-
import { join as joinPaths } from 'path';
1+
import { MsiInstaller, Urls } from './installers';
62

7-
const x64_URL = 'https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi';
8-
const x86_URL = 'https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x86/sqlncli.msi';
9-
10-
export default async function installNativeClient(version: number) {
11-
if (version !== 11) {
12-
throw new Error('Unsupported Native Client version, only 11 is valid.');
13-
}
14-
const arch = process.arch === 'x64' ? 'x64' : 'x86';
15-
let path = tc.find('sqlncli', '11.0', arch);
16-
if (!path) {
17-
core.info(`Downloading client installer for ${arch}.`);
18-
path = await downloadTool(arch === 'x64' ? x64_URL : x86_URL).then((tmp) => {
19-
return tc.cacheFile(tmp, 'sqlncli.msi', 'sqlncli', '11.0', arch);
20-
});
21-
} else {
22-
core.info('Loaded client installer from cache.');
3+
const VERSIONS = new Map<string, Urls>([
4+
['11', {
5+
x64: 'https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi',
6+
x86: 'https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x86/sqlncli.msi',
7+
}],
8+
]);
9+
export default async function installNativeClient(version: string) {
10+
if (!VERSIONS.has(version)) {
11+
throw new TypeError(`Invalid native client version supplied ${version}. Must be one of ${Array.from(VERSIONS.keys()).join(', ')}.`);
2312
}
24-
path = joinPaths(path, 'sqlncli.msi');
25-
core.info('Installing SQL Native Client 11.0');
2613
// see https://learn.microsoft.com/en-us/previous-versions/sql/sql-server-2012/ms131321(v=sql.110)
27-
await exec.exec('msiexec', [
28-
'/passive',
29-
'/i',
30-
path,
31-
'APPGUID={0CC618CE-F36A-415E-84B4-FB1BFF6967E1}',
32-
'IACCEPTSQLNCLILICENSETERMS=YES',
33-
], {
34-
windowsVerbatimArguments: true,
14+
const installer = new MsiInstaller({
15+
name: 'sqlncli',
16+
urls: VERSIONS.get(version)!,
17+
appGuid: '0CC618CE-F36A-415E-84B4-FB1BFF6967E1',
18+
version,
19+
extraArgs: [
20+
'IACCEPTSQLNCLILICENSETERMS=YES',
21+
],
3522
});
23+
return installer.install();
3624
}

src/install.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default async function install() {
7777
}
7878
}
7979
if (nativeClientVersion) {
80-
await core.group('Installing SQL Native Client', () => installNativeClient(parseInt(nativeClientVersion, 10)));
80+
await core.group('Installing SQL Native Client', () => installNativeClient(nativeClientVersion));
8181
}
8282
// Initial checks complete - fetch the installer
8383
const toolPath = await core.group(`Fetching install media for ${version}`, () => findOrDownloadTool(config));

src/installers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './msi-installer';

src/installers/installer.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as core from '@actions/core';
2+
import * as tc from '@actions/tool-cache';
3+
import { basename, dirname, extname, join as joinPaths } from 'path';
4+
import * as io from '@actions/io';
5+
6+
export interface InstallerConfig {
7+
name: string;
8+
version: string;
9+
}
10+
11+
export abstract class Installer {
12+
public readonly name: string;
13+
public readonly version: string;
14+
15+
constructor(config: InstallerConfig) {
16+
this.name = config.name;
17+
this.version = config.version;
18+
}
19+
20+
protected getArch(): string {
21+
return process.arch === 'x32' ? 'x86' : 'x64';
22+
}
23+
24+
protected downloadInstaller(url: string, extName?: string) {
25+
core.debug(`Downloading from ${url}`);
26+
return tc.downloadTool(url).then((path) => {
27+
const ext = extName ?? extname(url);
28+
const downloadDir = dirname(path);
29+
const destination = joinPaths(downloadDir, `${basename(path, `${ext}`)}${ext}`);
30+
return io.mv(path, destination).then(() => {
31+
core.debug(`Downloaded to ${destination}`);
32+
return destination;
33+
});
34+
});
35+
}
36+
37+
abstract install(): Promise<void>;
38+
}

src/installers/msi-installer.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as core from '@actions/core';
2+
import * as tc from '@actions/tool-cache';
3+
import { Installer, InstallerConfig } from './installer';
4+
import { join as joinPaths } from 'path';
5+
import * as exec from '@actions/exec';
6+
7+
export interface Urls {
8+
x64?: string;
9+
x86?: string;
10+
}
11+
12+
export interface MsiInstallerConfig extends InstallerConfig {
13+
urls: Urls;
14+
appGuid?: string;
15+
extraArgs?: string[];
16+
silent?: boolean;
17+
}
18+
19+
export class MsiInstaller extends Installer {
20+
private readonly urls: Urls;
21+
private readonly guid: string | undefined;
22+
private readonly silent: boolean;
23+
private readonly extraArgs: string[];
24+
constructor(config: MsiInstallerConfig) {
25+
super(config);
26+
this.urls = { ...config.urls };
27+
this.guid = config.appGuid;
28+
this.silent = config.silent ?? true;
29+
this.extraArgs = config.extraArgs ?? [];
30+
}
31+
32+
private get installUrl(): string {
33+
const arch = this.getArch();
34+
if (this.urls[arch]) {
35+
return this.urls[arch];
36+
}
37+
return Object.values(this.urls).find((val) => val && typeof val === 'string');
38+
}
39+
40+
public async install() {
41+
let path = tc.find(this.name, this.version, this.getArch());
42+
if (path) {
43+
core.info(`Found ${this.name} installer in cache @ ${path}`);
44+
} else {
45+
core.info(`Download ${this.name} installer from ${this.installUrl}`);
46+
path = await this.downloadInstaller(this.installUrl, '.msi').then((tmp) => {
47+
return tc.cacheFile(tmp, `${this.name}.msi`, this.name, this.version);
48+
});
49+
core.info(`Downloaded ${this.name} installer to cache @ ${path}`);
50+
}
51+
path = joinPaths(path, `${this.name}.msi`);
52+
core.info('Running installer');
53+
const args: string[] = [
54+
'/i',
55+
path,
56+
];
57+
if (this.guid) {
58+
args.push(`APPGUID={${this.guid}}`);
59+
}
60+
if (this.extraArgs.length) {
61+
args.push(...this.extraArgs);
62+
}
63+
if (this.silent) {
64+
args.unshift('/passive');
65+
}
66+
await exec.exec('msiexec', args, {
67+
windowsVerbatimArguments: true,
68+
});
69+
core.info('Install complete');
70+
}
71+
}

test/install-native-client.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { match, restore, SinonStubbedInstance, stub } from 'sinon';
22
import * as core from '@actions/core';
33
import * as tc from '@actions/tool-cache';
44
import * as exec from '@actions/exec';
5-
import * as utils from '../src/utils';
5+
import * as io from '@actions/io';
66
import installNativeClient from '../src/install-native-client';
77
import { expect, use } from 'chai';
88
import sinonChai from 'sinon-chai';
@@ -12,7 +12,7 @@ describe('install-native-client', () => {
1212
// let coreStub: SinonStubbedInstance<typeof core>;
1313
let tcStub: SinonStubbedInstance<typeof tc>;
1414
let execStub: SinonStubbedInstance<typeof exec>;
15-
let utilsStub: SinonStubbedInstance<typeof utils>;
15+
let ioStub: SinonStubbedInstance<typeof io>;
1616
let arch: PropertyDescriptor;
1717
beforeEach('stub deps', () => {
1818
stub(core);
@@ -21,8 +21,8 @@ describe('install-native-client', () => {
2121
tcStub.find.returns('');
2222
execStub = stub(exec);
2323
execStub.exec.resolves();
24-
utilsStub = stub(utils);
25-
utilsStub.downloadTool.resolves('c:/tmp/downloads');
24+
ioStub = stub(io);
25+
ioStub.mv.resolves();
2626
});
2727
afterEach('restore stubs', () => {
2828
Object.defineProperty(process, 'arch', arch);
@@ -31,17 +31,17 @@ describe('install-native-client', () => {
3131
describe('.installNativeClient()', () => {
3232
it('throws for bad version', async () => {
3333
try {
34-
await installNativeClient(10);
34+
await installNativeClient('10');
3535
} catch (e) {
36-
expect(e).to.have.property('message', 'Unsupported Native Client version, only 11 is valid.');
36+
expect(e).to.have.property('message', 'Invalid native client version supplied 10. Must be one of 11.');
3737
return;
3838
}
3939
expect.fail('expected to throw');
4040
});
4141
it('installs from cache', async () => {
4242
tcStub.find.returns('C:/tmp/');
43-
await installNativeClient(11);
44-
expect(utilsStub.downloadTool).to.have.callCount(0);
43+
await installNativeClient('11');
44+
expect(tcStub.downloadTool).to.have.callCount(0);
4545
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', match.array, {
4646
windowsVerbatimArguments: true,
4747
});
@@ -52,8 +52,9 @@ describe('install-native-client', () => {
5252
value: 'x64',
5353
});
5454
tcStub.cacheFile.resolves('C:/tmp/cache/');
55-
await installNativeClient(11);
56-
expect(utilsStub.downloadTool).to.have.been.calledOnceWith('https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi');
55+
tcStub.downloadTool.resolves('C:/tmp/downloads');
56+
await installNativeClient('11');
57+
expect(tcStub.downloadTool).to.have.been.calledOnceWith('https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi');
5758
expect(tcStub.cacheFile).to.have.callCount(1);
5859
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', match.array, {
5960
windowsVerbatimArguments: true,
@@ -65,8 +66,9 @@ describe('install-native-client', () => {
6566
value: 'x32',
6667
});
6768
tcStub.cacheFile.resolves('C:/tmp/cache/');
68-
await installNativeClient(11);
69-
expect(utilsStub.downloadTool).to.have.been.calledOnceWith('https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x86/sqlncli.msi');
69+
tcStub.downloadTool.resolves('C:/tmp/downloads');
70+
await installNativeClient('11');
71+
expect(tcStub.downloadTool).to.have.been.calledOnceWith('https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x86/sqlncli.msi');
7072
expect(tcStub.cacheFile).to.have.callCount(1);
7173
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', match.array, {
7274
windowsVerbatimArguments: true,

0 commit comments

Comments
 (0)