• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / boxcli / 18978830630

31 Oct 2025 04:28PM UTC coverage: 82.696% (-2.9%) from 85.581%
18978830630

Pull #603

github

web-flow
Merge a759265da into 0dffced54
Pull Request #603: feat: POC auto update CLI

1261 of 1757 branches covered (71.77%)

Branch coverage included in aggregate %.

13 of 166 new or added lines in 2 files covered. (7.83%)

146 existing lines in 3 files now uncovered.

4622 of 5357 relevant lines covered (86.28%)

611.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

3.56
/src/github-updater.js
1
'use strict';
2

3
const { ux } = require('@oclif/core');
9✔
4
const makeDebug = require('debug');
9✔
5
const { Updater } = require('@oclif/plugin-update/lib/update');
9✔
6

7
const debug = makeDebug('oclif:update:github');
9✔
8

9
let octokitInstance = null;
9✔
10
let octokitClass = null;
9✔
11
let gotModule = null;
9✔
12

13
async function loadOctokit() {
NEW
14
        if (!octokitClass) {
×
NEW
15
                const { Octokit } = await import('@octokit/rest');
×
NEW
16
                octokitClass = Octokit;
×
17
        }
NEW
18
        return octokitClass;
×
19
}
20

21
async function loadGot() {
NEW
22
        if (!gotModule) {
×
NEW
23
                const module = await import('got');
×
NEW
24
                gotModule = module.default || module;
×
25
        }
26

NEW
27
        return gotModule;
×
28
}
29

30
async function getOctokit() {
NEW
31
        if (!octokitInstance) {
×
NEW
32
                octokitClass = await loadOctokit();
×
NEW
33
                return new octokitClass({
×
34
                        auth: process.env.GITHUB_TOKEN || process.env.GH_TOKEN,
×
35
                });
36
        }
37
}
38

39
function checkGitHubConfig(config) {
NEW
40
        const githubConfig = config.pjson.oclif.update?.github;
×
41

NEW
42
        if (
×
43
                githubConfig &&
×
44
                typeof githubConfig === 'object' &&
45
                'owner' in githubConfig &&
46
                'repo' in githubConfig
47
        ) {
NEW
48
                return {
×
49
                        owner: githubConfig.owner,
50
                        repo: githubConfig.repo,
51
                };
52
        }
53

54
        // Try to parse from repository field
NEW
55
        const repo = config.pjson.repository;
×
NEW
56
        if (typeof repo === 'string') {
×
57
                // Handle formats like "owner/repo" or "github:owner/repo" or "https://github.com/owner/repo"
58
                const match =
NEW
59
                        repo.match(/^(?:github:)?([^/]+)\/([^/]+?)(?:\.git)?$/u) ||
×
60
                        repo.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/u);
61

NEW
62
                if (match) {
×
NEW
63
                        return {
×
64
                                owner: match[1],
65
                                repo: match[2],
66
                        };
67
                }
68
        }
69

NEW
70
        return null;
×
71
}
72

73
class GitHubUpdater extends Updater {
74
        constructor(config) {
NEW
75
                super(config);
×
NEW
76
                this.githubConfig = this.getGitHubConfig();
×
NEW
77
                this.octokit = null;
×
78
        }
79

80
        async ensureOctokit() {
NEW
81
                if (!this.octokit) {
×
NEW
82
                        this.octokit = await getOctokit();
×
83
                }
84
        }
85

86
        // Override runUpdate to use GitHub-specific methods
87
        // Since the base class has private methods for fetching manifests,
88
        // we need to override the entire runUpdate to use GitHub APIs
89
        async runUpdate(options) {
NEW
90
                const { autoUpdate, version, force = false } = options;
×
91

NEW
92
                if (autoUpdate) {
×
NEW
93
                        await this.debounce();
×
94
                }
95

NEW
96
                ux.action.start(`${this.config.name}: Updating CLI`);
×
97

NEW
98
                if (this.notUpdatable()) {
×
NEW
99
                        ux.action.stop('not updatable');
×
NEW
100
                        return;
×
101
                }
102

NEW
103
                const channel = options.channel || (await this.determineChannel(version));
×
NEW
104
                const current = await this.determineCurrentVersion();
×
105

NEW
106
                if (version) {
×
NEW
107
                        const localVersion = force ? null : await this.findLocalVersion(version);
×
108

NEW
109
                        if (this.alreadyOnVersion(current, localVersion || null)) {
×
NEW
110
                                ux.action.stop(
×
111
                                        this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')
×
112
                                                ? 'done'
113
                                                : `already on version ${current}`,
114
                                );
NEW
115
                                return;
×
116
                        }
117

NEW
118
                        await this.config.runHook('preupdate', { channel, version });
×
119

NEW
120
                        if (localVersion) {
×
NEW
121
                                await this.updateToExistingVersion(current, localVersion);
×
122
                        } else {
NEW
123
                                const index = await this.fetchVersionIndex();
×
NEW
124
                                const url = index[version];
×
NEW
125
                                if (!url) {
×
NEW
126
                                        throw new Error(
×
127
                                                `${version} not found in index:\n${Object.keys(index).join(', ')}`,
128
                                        );
129
                                }
130

NEW
131
                                const manifest = await this.fetchGitHubVersionManifest(version, url);
×
NEW
132
                                const updated = manifest.sha
×
133
                                        ? `${manifest.version}-${manifest.sha}`
134
                                        : manifest.version;
NEW
135
                                await this.update(manifest, current, updated, force, channel);
×
136
                        }
137

NEW
138
                        await this.config.runHook('update', { channel, version });
×
NEW
139
                        ux.action.stop();
×
NEW
140
                        ux.log();
×
NEW
141
                        ux.log(
×
142
                                `Updating to a specific version will not update the channel. If autoupdate is enabled, the CLI will eventually be updated back to ${channel}.`,
143
                        );
144
                } else {
NEW
145
                        const manifest = await this.fetchGitHubChannelManifest(channel);
×
NEW
146
                        const updated = manifest.sha
×
147
                                ? `${manifest.version}-${manifest.sha}`
148
                                : manifest.version;
149

NEW
150
                        if (!force && this.alreadyOnVersion(current, updated)) {
×
NEW
151
                                ux.action.stop(
×
152
                                        this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')
×
153
                                                ? 'done'
154
                                                : `already on version ${current}`,
155
                                );
156
                        } else {
NEW
157
                                await this.config.runHook('preupdate', { channel, version: updated });
×
NEW
158
                                await this.update(manifest, current, updated, force, channel);
×
159
                        }
160

NEW
161
                        await this.config.runHook('update', { channel, version: updated });
×
NEW
162
                        ux.action.stop();
×
163
                }
164

NEW
165
                await this.touch();
×
NEW
166
                await this.tidy();
×
167
        }
168

169
        // Override fetchVersionIndex to use GitHub releases
170
        async fetchVersionIndex() {
NEW
171
                return this.fetchGitHubVersionIndex();
×
172
        }
173

174
        // GitHub-specific implementation
175
        async fetchGitHubVersionIndex() {
NEW
176
                await this.ensureOctokit();
×
NEW
177
                ux.action.status = 'fetching version index from GitHub';
×
178

NEW
179
                const { owner, repo } = this.githubConfig;
×
180

NEW
181
                try {
×
NEW
182
                debug(`Fetching releases for ${owner}/${repo}`);
×
NEW
183
                const { data: releases } = await this.octokit.repos.listReleases({
×
184
                        owner,
185
                        per_page: 100,
186
                        repo,
187
                });
188

NEW
189
                        const versionIndex = {};
×
190

NEW
191
                        for (const release of releases) {
×
192
                                // Extract version from tag_name (remove 'v' prefix if present)
NEW
193
                                const version = release.tag_name.replace(/^v/u, '');
×
194

195
                                // Find the appropriate asset for this platform/arch
NEW
196
                                const assetName = this.determineAssetName(version);
×
NEW
197
                                const asset = release.assets.find((a) => a.name === assetName);
×
198

NEW
199
                                if (asset) {
×
NEW
200
                                        versionIndex[version] = asset.browser_download_url;
×
201
                                }
202
                        }
203

NEW
204
                        debug(`Found ${Object.keys(versionIndex).length} versions`);
×
NEW
205
                        return versionIndex;
×
206
                } catch (error) {
NEW
207
                        debug('Failed to fetch GitHub releases', error);
×
NEW
208
                        throw new Error(
×
209
                                `Failed to fetch releases from GitHub repository ${owner}/${repo}`,
210
                        );
211
                }
212
        }
213

214
        // GitHub-specific channel manifest fetching
215
        async fetchGitHubChannelManifest(channel) {
NEW
216
                await this.ensureOctokit();
×
NEW
217
                ux.action.status = 'fetching manifest from GitHub';
×
218

NEW
219
                const { owner, repo } = this.githubConfig;
×
220

NEW
221
                try {
×
222
                        let release;
223

NEW
224
                        if (channel === 'stable') {
×
NEW
225
                                debug(`Fetching latest release for ${owner}/${repo}`);
×
NEW
226
                                const { data } = await this.octokit.repos.getLatestRelease({
×
227
                                        owner,
228
                                        repo,
229
                                });
NEW
230
                                release = data;
×
231
                        } else {
NEW
232
                                debug(`Fetching release ${channel} for ${owner}/${repo}`);
×
NEW
233
                                const { data } = await this.octokit.repos.getReleaseByTag({
×
234
                                        owner,
235
                                        repo,
236
                                        tag: channel,
237
                                });
NEW
238
                                release = data;
×
239
                        }
240

NEW
241
                        const version = release.tag_name.replace(/^v/u, '');
×
NEW
242
                        const manifestName = this.determineManifestName();
×
NEW
243
                        const manifestAsset = release.assets.find((a) => a.name === manifestName);
×
244

NEW
245
                if (manifestAsset) {
×
246
                        // Fetch the manifest file using got (since it's a direct download URL)
NEW
247
                        debug(`Fetching manifest from ${manifestAsset.browser_download_url}`);
×
NEW
248
                        const got = await loadGot();
×
NEW
249
                        const { body } = await got.get(manifestAsset.browser_download_url);
×
NEW
250
                        return typeof body === 'string' ? JSON.parse(body) : body;
×
251
                }
252

253
                        // If no manifest found, construct a basic one
NEW
254
                        const assetName = this.determineAssetName(version);
×
NEW
255
                        const asset = release.assets.find((a) => a.name === assetName);
×
256

NEW
257
                        if (!asset) {
×
NEW
258
                                const config = this.config;
×
NEW
259
                                throw new Error(
×
260
                                        `No suitable asset found for ${config.platform}-${config.arch} in release ${release.tag_name}`,
261
                                );
262
                        }
263

NEW
264
                        return {
×
265
                                gz: asset.browser_download_url,
266
                                version,
267
                        };
268
                } catch (error) {
NEW
269
                        const statusCode = error.status;
×
NEW
270
                        if (statusCode === 404) {
×
NEW
271
                                throw new Error(
×
272
                                        `Release not found for channel "${channel}" in ${owner}/${repo}`,
273
                                );
274
                        }
275

NEW
276
                        throw error;
×
277
                }
278
        }
279

280
        // GitHub-specific version manifest fetching
281
        async fetchGitHubVersionManifest(version, url) {
NEW
282
                await this.ensureOctokit();
×
NEW
283
                ux.action.status = 'fetching version manifest from GitHub';
×
284

NEW
285
                const { owner, repo } = this.githubConfig;
×
286

NEW
287
                try {
×
NEW
288
                        debug(`Fetching release v${version} for ${owner}/${repo}`);
×
NEW
289
                        const { data: release } = await this.octokit.repos.getReleaseByTag({
×
290
                                owner,
291
                                repo,
292
                                tag: `v${version}`,
293
                        });
294

NEW
295
                const manifestName = this.determineManifestName();
×
NEW
296
                const manifestAsset = release.assets.find((a) => a.name === manifestName);
×
297

NEW
298
                if (manifestAsset) {
×
NEW
299
                        debug(`Fetching manifest from ${manifestAsset.browser_download_url}`);
×
NEW
300
                        const got = await loadGot();
×
NEW
301
                        const { body } = await got.get(manifestAsset.browser_download_url);
×
NEW
302
                        return typeof body === 'string' ? JSON.parse(body) : body;
×
303
                }
304

305
                        // If no manifest found, construct a basic one
NEW
306
                        return {
×
307
                                gz: url,
308
                                version,
309
                        };
310
                } catch (error) {
NEW
311
                        debug('Failed to fetch version manifest', error);
×
312
                        // Return a basic manifest using the URL we have
NEW
313
                        return {
×
314
                                gz: url,
315
                                version,
316
                        };
317
                }
318
        }
319

320
        determineAssetName(version) {
NEW
321
                const config = this.config;
×
NEW
322
                const platform = config.platform === 'wsl' ? 'linux' : config.platform;
×
NEW
323
                const ext = config.windows ? 'tar.gz' : 'tar.gz';
×
NEW
324
                return `${config.bin}-v${version}-${platform}-${config.arch}.${ext}`;
×
325
        }
326

327
        determineManifestName() {
NEW
328
                const config = this.config;
×
NEW
329
                const platform = config.platform === 'wsl' ? 'linux' : config.platform;
×
NEW
330
                return `${config.bin}-${platform}-${config.arch}-buildmanifest`;
×
331
        }
332

333
        getGitHubConfig() {
NEW
334
                const oclifConfig = this.config;
×
NEW
335
                const config = checkGitHubConfig(oclifConfig);
×
NEW
336
                if (!config) {
×
NEW
337
                        throw new Error(
×
338
                                'GitHub repository not configured. Add "oclif.update.github" with "owner" and "repo" fields to package.json',
339
                        );
340
                }
341

NEW
342
                return config;
×
343
        }
344
}
345

346
module.exports = GitHubUpdater;
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc