-
Notifications
You must be signed in to change notification settings - Fork 55
/
publish.js
206 lines (170 loc) · 6.66 KB
/
publish.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*!
wow.export (https://github.com/Kruithne/wow.export)
Authors: Kruithne <[email protected]>
License: MIT
*/
const chalk = require('chalk');
const path = require('path');
const util = require('util');
const fs = require('fs');
const fsp = fs.promises;
const argv = process.argv.splice(2);
const AdmZip = require('adm-zip');
const SFTPClient = require('ssh2-sftp-client');
/**
* Defines the location of the build configuration file.
* @type {string}
*/
const BUILD_CONFIG_FILE = path.join(__dirname, 'build.conf');
/**
* Defines the location of the publish configuration file.
* @type {string}
*/
const PUBLISH_CONFIG_FILE = path.join(__dirname, 'publish.conf');
/**
* Defines the build manifest file, relative to the build directory.
* @type {string}
*/
const MANIFEST_FILE = 'package.json';
/**
* Defines the build directory.
* @type {string}
*/
const BUILD_DIR = path.join(__dirname, 'bin');
/**
* Pattern used to locate text surrounded by curly brackets in a string.
* @type {RegExp}
*/
const HIGHLIGHT_PATTERN = /{([^}]+)}/g;
/**
* Highlights all text in `message` contained within curly brackets by
* calling `colorFunc` on each and substituting the result.
* @param {string} message String that will be highlighted.
* @param {function} colorFunc Colouring function.
*/
const filterHighlights = (message, colorFunc) => {
return message.replace(HIGHLIGHT_PATTERN, (_, g1) => colorFunc(g1));
};
/**
* Logging utility.
* @type {Object<string, function>}
*/
const log = {
error: (msg, ...params) => log.print('{ERR} ' + msg, chalk.red, ...params),
warn: (msg, ...params) => log.print('{WARN} ' + msg, chalk.yellow, ...params),
success: (msg, ...params) => log.print('{DONE} ' + msg, chalk.green, ...params),
info: (msg, ...params) => log.print('{INFO} ' + msg, chalk.blue, ...params),
print: (msg, colorFunc, ...params) => console.log(filterHighlights(msg, colorFunc), ...params)
};
(async () => {
let sftp;
try {
const buildConfig = JSON.parse(await fsp.readFile(BUILD_CONFIG_FILE));
const publishConfig = JSON.parse(await fsp.readFile(PUBLISH_CONFIG_FILE));
let sftpConfig = {
host: process.env.SFTP_HOST,
port: process.env.SFTP_PORT ?? 22,
username: process.env.SFTP_USER,
password: process.env.SFTP_PASS,
privateKey: process.env.SFTP_PRIVATE_KEY,
remoteUpdateDir: process.env.SFTP_REMOTE_UPDATE_DIR,
remotePackageDir: process.env.SFTP_REMOTE_PACKAGE_DIR
};
// Collect available build names.
const builds = buildConfig.builds.map(build => build.name);
// Check all provided CLI parameters for valid build names.
const targetBuilds = [];
if (argv.includes('*')) {
// If * is present as a parameter, include all builds.
targetBuilds.push(...builds);
} else {
for (let arg of argv) {
arg = arg.toLowerCase();
if (builds.includes(arg)) {
if (!targetBuilds.includes(arg))
targetBuilds.push(arg);
else
log.warn('Duplicate build {%s} provided in arguments, only publishing once.', arg);
} else {
log.error('Unknown build {%s}, check build configuration.', arg);
return;
}
}
}
// User has not selected any valid builds; display available and exit.
if (targetBuilds.length === 0) {
log.warn('You have not selected any builds.');
log.info('Available builds: ' + builds.map(e => '{' + e + '}').join(', '));
return;
}
const uploads = [];
const publishStart = Date.now();
log.info('Selected builds: ' + targetBuilds.map(e => '{' + e + '}').join(', '));
for (const build of targetBuilds) {
const publishBuildStart = Date.now();
const buildDir = path.join(BUILD_DIR, build);
const buildManifestPath = path.join(buildDir, MANIFEST_FILE);
const buildManifest = JSON.parse(await fsp.readFile(buildManifestPath));
log.info('Packaging {%s} ({%s})...', buildManifest.version, buildManifest.guid);
// Prepare update files for upload.
for (const file of publishConfig.updateFiles) {
const remote = util.format(sftpConfig.remoteUpdateDir, build, file);
const local = path.join(buildDir, file);
uploads.push({ local, remote, tmpProtection: true });
}
const zip = new AdmZip();
zip.addLocalFolder(buildDir, '', entry => {
// Do not package update files with the download archive.
return !publishConfig.updateFiles.includes(entry)
});
const packageName = util.format(publishConfig.packageName, buildManifest.version);
const packageOut = path.join(publishConfig.packageOut, packageName);
// Ensure directories exist for the package.e
await fsp.mkdir(path.dirname(packageOut), { recursive: true });
log.info('Writing package {%s}...', packageOut);
zip.writeZip(packageOut);
// Store the package path for upload.
const remoteFile = util.format(sftpConfig.remotePackageDir, build, packageName);
uploads.push({ remote: remoteFile, local: packageOut });
const publishBuildElapsed = (Date.now() - publishBuildStart) / 1000;
log.success('Build {%s} version {%s} packaged in {%ds}', build, buildManifest.version, publishBuildElapsed);
}
if (uploads.length > 0) {
const uploadStart = Date.now();
log.info('Establishing SFTP connection to {%s} @ {%d}', sftpConfig.host, sftpConfig.port);
// Load private key from disk if defined.
if (typeof sftpConfig.privateKey === 'string')
sftpConfig.privateKey = await fsp.readFile(sftpConfig.privateKey);
sftp = new SFTPClient();
await sftp.connect(sftpConfig);
const renames = new Map();
for (const upload of uploads) {
log.info('Uploading {%s} to {%s}...', upload.local, upload.remote);
if (upload.tmpProtection) {
// Upload as a temporary file then rename on the server.
const tmpRemote = upload.remote + '.tmp';
await sftp.mkdir(path.dirname(upload.remote), true);
await sftp.put(upload.local, tmpRemote);
renames.set(tmpRemote, upload.remote);
} else {
// Upload files normally.
await sftp.mkdir(path.dirname(upload.remote), true);
await sftp.put(upload.local, upload.remote);
}
}
log.info('Renaming remote temporary files...');
for (const [from, to] of renames)
await sftp.posixRename(from, to);
const uploadElapsed = (Date.now() - uploadStart) / 1000;
log.success('Uploaded {%d} files in {%ds}', uploads.length, uploadElapsed);
}
const publishElapsed = (Date.now() - publishStart) / 1000;
log.success('Published all packages in {%ds}', publishElapsed);
} catch (e) {
log.error('Publish failed due to error: %s', e.message);
log.error(e.stack);
} finally {
if (sftp)
await sftp.end();
}
})();