-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathpublishing.ts
209 lines (180 loc) · 5.61 KB
/
publishing.ts
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
207
208
209
import { AssetManifest, IManifestEntry } from './asset-manifest';
import { IAws } from './aws';
import { IHandlerHost } from './private/asset-handler';
import { DockerFactory } from './private/docker';
import { makeAssetHandler } from './private/handlers';
import { EventType, IPublishProgress, IPublishProgressListener } from './progress';
export interface AssetPublishingOptions {
/**
* Entry point for AWS client
*/
readonly aws: IAws;
/**
* Listener for progress events
*
* @default No listener
*/
readonly progressListener?: IPublishProgressListener;
/**
* Whether to throw at the end if there were errors
*
* @default true
*/
readonly throwOnError?: boolean;
/**
* Whether to publish in parallel
*
* @default false
*/
readonly publishInParallel?: boolean;
/**
* Whether to build assets
*
* @default true
*/
readonly buildAssets?: boolean;
/**
* Whether to publish assets
*
* @default true
*/
readonly publishAssets?: boolean;
}
/**
* A failure to publish an asset
*/
export interface FailedAsset {
/**
* The asset that failed to publish
*/
readonly asset: IManifestEntry;
/**
* The failure that occurred
*/
readonly error: Error;
}
export class AssetPublishing implements IPublishProgress {
/**
* The message for the IPublishProgress interface
*/
public message: string = 'Starting';
/**
* The current asset for the IPublishProgress interface
*/
public currentAsset?: IManifestEntry;
public readonly failures = new Array<FailedAsset>();
private readonly assets: IManifestEntry[];
private readonly totalOperations: number;
private completedOperations: number = 0;
private aborted = false;
private readonly handlerHost: IHandlerHost;
private readonly publishInParallel: boolean;
private readonly buildAssets: boolean;
private readonly publishAssets: boolean;
private readonly startMessagePrefix: string;
private readonly successMessagePrefix: string;
private readonly errorMessagePrefix: string;
constructor(private readonly manifest: AssetManifest, private readonly options: AssetPublishingOptions) {
this.assets = manifest.entries;
this.totalOperations = this.assets.length;
this.publishInParallel = options.publishInParallel ?? false;
this.buildAssets = options.buildAssets ?? true;
this.publishAssets = options.publishAssets ?? true;
const getMessages = () => {
if (this.buildAssets && this.publishAssets) {
return {
startMessagePrefix: 'Building and publishing',
successMessagePrefix: 'Built and published',
errorMessagePrefix: 'Error building and publishing',
};
} else if (this.buildAssets) {
return {
startMessagePrefix: 'Building',
successMessagePrefix: 'Built',
errorMessagePrefix: 'Error building',
};
} else {
return {
startMessagePrefix: 'Publishing',
successMessagePrefix: 'Published',
errorMessagePrefix: 'Error publishing',
};
}
};
const messages = getMessages();
this.startMessagePrefix = messages.startMessagePrefix;
this.successMessagePrefix = messages.successMessagePrefix;
this.errorMessagePrefix = messages.errorMessagePrefix;
const self = this;
this.handlerHost = {
aws: this.options.aws,
get aborted() { return self.aborted; },
emitMessage(t, m) { self.progressEvent(t, m); },
dockerFactory: new DockerFactory(),
};
}
/**
* Publish all assets from the manifest
*/
public async publish(): Promise<void> {
if (this.publishInParallel) {
await Promise.all(this.assets.map(async (asset) => this.publishAsset(asset)));
} else {
for (const asset of this.assets) {
if (!await this.publishAsset(asset)) {
break;
}
}
}
if ((this.options.throwOnError ?? true) && this.failures.length > 0) {
throw new Error(`${this.errorMessagePrefix}: ${this.failures.map(e => e.error.message)}`);
}
}
/**
* Publish an asset.
* @param asset The asset to publish
* @returns false when publishing should stop
*/
private async publishAsset(asset: IManifestEntry) {
try {
if (this.progressEvent(EventType.START, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
const handler = makeAssetHandler(this.manifest, asset, this.handlerHost);
if (this.buildAssets) {
await handler.build();
}
if (this.publishAssets) {
await handler.publish();
}
if (this.aborted) {
throw new Error('Aborted');
}
this.completedOperations++;
if (this.progressEvent(EventType.SUCCESS, `${this.successMessagePrefix} ${asset.id}`)) { return false; }
} catch (e) {
this.failures.push({ asset, error: e });
this.completedOperations++;
if (this.progressEvent(EventType.FAIL, e.message)) { return false; }
}
return true;
}
public get percentComplete() {
if (this.totalOperations === 0) { return 100; }
return Math.floor((this.completedOperations / this.totalOperations) * 100);
}
public abort(): void {
this.aborted = true;
}
public get hasFailures() {
return this.failures.length > 0;
}
/**
* Publish a progress event to the listener, if present.
*
* Returns whether an abort is requested. Helper to get rid of repetitive code in publish().
*/
private progressEvent(event: EventType, message: string): boolean {
this.message = message;
if (this.options.progressListener) { this.options.progressListener.onPublishEvent(event, this); }
return this.aborted;
}
}