Skip to content

Commit 0db9b7e

Browse files
authored
Merge pull request tediousjs#5 from dhensby/pulls/installers
feat: add ODBC driver install step
2 parents 7694a7e + 62dde57 commit 0db9b7e

19 files changed

+651
-58
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ jobs:
146146
with:
147147
sqlserver-version: ${{ matrix.sqlserver }}
148148
native-client-version: 11
149+
odbc-version: 17
149150
release:
150151
name: Release
151152
concurrency: release

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,25 @@ See [action.yml](./action.yml):
1111
with:
1212
# Skip OS checks that will stop installation attempts preemptively.
1313
# Default: false
14-
skip-os-check: ''
14+
skip-os-check: false
1515

1616
# Version to use. Examples: 2008, 2012, 2014, etc. "latest" can also be used.
1717
# Default: latest
18-
sqlserver-version: ''
18+
sqlserver-version: 'latest'
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

23+
# Version of ODBC to install. Supported versions: 17, 18.
24+
odbc-version: ''
25+
2326
# The SA user password to use.
2427
# Default: yourStrong(!)Password
25-
sa-password: ''
28+
sa-password: 'yourStrong(!)Password'
2629

2730
# The database collation to use.
2831
# Default: SQL_Latin1_General_CP1_CI_AS
29-
db-collation: ''
32+
db-collation: 'SQL_Latin1_General_CP1_CI_AS'
3033

3134
# Any custom install arguments you wish to use. These must be in the format of
3235
# "/ARG=VAL".
@@ -35,7 +38,7 @@ See [action.yml](./action.yml):
3538
# Wait for the database to respond successfully to queries before completing the
3639
# action. A maximum of 10 attempts is made.
3740
# Default: true
38-
wait-for-ready: ''
41+
wait-for-ready: true
3942
```
4043
<!-- end usage -->
4144

action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ 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.'
13+
odbc-version:
14+
description: 'Version of ODBC to install. Supported versions: 17, 18.'
1315
sa-password:
1416
description: 'The SA user password to use.'
1517
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.

misc/generate-docs.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function updateUsage(
9595
// Constrain the width of the description
9696
const width = 80;
9797
let description = (input.description as string)
98-
.trimRight()
98+
.trimEnd()
9999
.replace(/\r\n/g, '\n') // Convert CR to LF
100100
.replace(/ +/g, ' ') // Squash consecutive spaces
101101
.replace(/ \n/g, '\n'); // Squash space followed by newline
@@ -123,15 +123,15 @@ function updateUsage(
123123
}
124124

125125
// Append segment
126-
newReadme.push(` # ${segment}`.trimRight());
126+
newReadme.push(` # ${segment}`.trimEnd());
127127

128128
// Remaining
129129
description = description.substr(segment.length);
130130
}
131131

132132
if (input.default !== undefined) {
133133
// Append blank line if description had paragraphs
134-
if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) {
134+
if ((input.description as string).trimEnd().match(/\n[ ]*\r?\n/)) {
135135
newReadme.push(` #`);
136136
}
137137

@@ -140,7 +140,16 @@ function updateUsage(
140140
}
141141

142142
// Input name
143-
newReadme.push(` ${key}: ''`);
143+
let inputValue: string;
144+
switch (input.default) {
145+
case 'true':
146+
case 'false':
147+
inputValue = input.default;
148+
break;
149+
default:
150+
inputValue = `'${input.default ?? ''}'`;
151+
}
152+
newReadme.push(` ${key}: ${inputValue}`);
144153

145154
firstInput = false;
146155
}

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-odbc.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { MsiInstaller, Urls } from './installers';
2+
3+
// https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver16
4+
const VERSIONS = new Map<string, Urls>([
5+
['18', {
6+
x64: 'https://go.microsoft.com/fwlink/?linkid=2242886',
7+
x86: 'https://go.microsoft.com/fwlink/?linkid=2242980',
8+
}],
9+
['17', {
10+
x64: 'https://go.microsoft.com/fwlink/?linkid=2239168',
11+
x86: 'https://go.microsoft.com/fwlink/?linkid=2238791',
12+
}],
13+
]);
14+
15+
export default async function installOdbc(version: string) {
16+
if (!VERSIONS.has(version)) {
17+
throw new TypeError(`Invalid ODBC version supplied ${version}. Must be one of ${Array.from(VERSIONS.keys()).join(', ')}.`);
18+
}
19+
const installer = new MsiInstaller({
20+
name: 'msodbcsql',
21+
urls: VERSIONS.get(version)!,
22+
version,
23+
extraArgs: [
24+
'IACCEPTMSODBCSQLLICENSETERMS=YES',
25+
],
26+
});
27+
return installer.install();
28+
}

src/install.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
waitForDatabase,
1515
} from './utils';
1616
import installNativeClient from './install-native-client';
17+
import installOdbc from './install-odbc';
1718

1819
/**
1920
* Attempt to load the installer from the tool-cache, otherwise, fetch it.
@@ -42,6 +43,7 @@ export default async function install() {
4243
wait,
4344
skipOsCheck,
4445
nativeClientVersion,
46+
odbcVersion,
4547
} = gatherInputs();
4648
// we only support windows for now. But allow crazy people to skip this check if they like...
4749
if (!skipOsCheck && os.platform() !== 'win32') {
@@ -77,7 +79,10 @@ export default async function install() {
7779
}
7880
}
7981
if (nativeClientVersion) {
80-
await core.group('Installing SQL Native Client', () => installNativeClient(parseInt(nativeClientVersion, 10)));
82+
await core.group('Installing SQL Native Client', () => installNativeClient(nativeClientVersion));
83+
}
84+
if (odbcVersion) {
85+
await core.group('Installing ODBC', () => installOdbc(odbcVersion));
8186
}
8287
// Initial checks complete - fetch the installer
8388
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+
}

src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface Inputs {
4949
wait: boolean;
5050
skipOsCheck: boolean;
5151
nativeClientVersion: string;
52+
odbcVersion: string;
5253
}
5354

5455
/**
@@ -66,6 +67,7 @@ export function gatherInputs(): Inputs {
6667
wait: core.getBooleanInput('wait-for-ready'),
6768
skipOsCheck: core.getBooleanInput('skip-os-check'),
6869
nativeClientVersion: core.getInput('native-client-version'),
70+
odbcVersion: core.getInput('odbc-version'),
6971
};
7072
}
7173

0 commit comments

Comments
 (0)