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

box / boxcli / 19469863477

18 Nov 2025 02:35PM UTC coverage: 84.398% (-1.2%) from 85.581%
19469863477

Pull #603

github

web-flow
Merge 46ab619d2 into 6a32aa52c
Pull Request #603: feat: support auto update using github releases

1287 of 1745 branches covered (73.75%)

Branch coverage included in aggregate %.

103 of 172 new or added lines in 4 files covered. (59.88%)

1 existing line in 1 file now uncovered.

4696 of 5344 relevant lines covered (87.87%)

614.41 hits per line

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

53.33
/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

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

20
async function getOctokit() {
NEW
21
        if (!octokitInstance) {
×
NEW
22
                octokitClass = await loadOctokit();
×
NEW
23
                return new octokitClass({
×
24
                        auth: process.env.GITHUB_TOKEN || process.env.GH_TOKEN,
×
25
                });
26
        }
27
}
28

29
function checkGitHubConfig(config) {
30
        const githubConfig = config.pjson.oclif.update?.github;
324✔
31

32
        if (
324✔
33
                githubConfig &&
963✔
34
                typeof githubConfig === 'object' &&
35
                'owner' in githubConfig &&
36
                'repo' in githubConfig
37
        ) {
38
                return {
198✔
39
                        owner: githubConfig.owner,
40
                        repo: githubConfig.repo,
41
                };
42
        }
43

44
        // Try to parse from repository field
45
        const repo = config.pjson.repository;
126✔
46
        if (typeof repo === 'string') {
126✔
47
                // Handle formats like "owner/repo" or "github:owner/repo" or "https://github.com/owner/repo"
48
                const match =
49
                        repo.match(/^(?:github:)?([^/]+)\/([^/]+?)(?:\.git)?$/u) ||
99✔
50
                        repo.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/u);
51

52
                if (match) {
99✔
53
                        return {
90✔
54
                                owner: match[1],
55
                                repo: match[2],
56
                        };
57
                }
58
        }
59

60
        return null;
36✔
61
}
62

63
class GitHubUpdater extends Updater {
64
        constructor(config) {
65
                super(config);
261✔
66
                this.githubConfig = this.getGitHubConfig();
261✔
67
                this.octokit = null;
225✔
68
        }
69

70
        async ensureOctokit() {
71
                if (!this.octokit) {
81!
NEW
72
                        this.octokit = await getOctokit();
×
73
                }
74
        }
75

76
        // Override runUpdate to use GitHub-specific methods
77
        // Since the base class has private methods for fetching manifests,
78
        // we need to override the entire runUpdate to use GitHub APIs
79
        async runUpdate(options) {
NEW
80
                const { autoUpdate, version, force = false } = options;
×
81

NEW
82
                if (autoUpdate) {
×
NEW
83
                        await this.debounce();
×
84
                }
85

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

NEW
88
                if (this.notUpdatable()) {
×
NEW
89
                        ux.action.stop('not updatable');
×
NEW
90
                        return;
×
91
                }
92

93
                const channel =
NEW
94
                        options.channel || (await this.determineChannel(version));
×
NEW
95
                const current = await this.determineCurrentVersion();
×
96

NEW
97
                if (version) {
×
NEW
98
                        const localVersion = force
×
99
                                ? null
100
                                : await this.findLocalVersion(version);
101

NEW
102
                        if (this.alreadyOnVersion(current, localVersion || null)) {
×
NEW
103
                                ux.action.stop(
×
104
                                        this.config.scopedEnvVar('HIDE_UPDATED_MESSAGE')
×
105
                                                ? 'done'
106
                                                : `already on version ${current}`
107
                                );
NEW
108
                                return;
×
109
                        }
110

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

NEW
113
                        if (localVersion) {
×
NEW
114
                                await this.updateToExistingVersion(current, localVersion);
×
115
                        } else {
NEW
116
                                const index = await this.fetchVersionIndex();
×
NEW
117
                                const url = index[version];
×
NEW
118
                                if (!url) {
×
NEW
119
                                        throw new Error(
×
120
                                                `${version} not found in index:\n${Object.keys(index).join(', ')}`
121
                                        );
122
                                }
123

NEW
124
                                const manifest = await this.fetchGitHubVersionManifest(
×
125
                                        version,
126
                                        url
127
                                );
NEW
128
                                const updated = manifest.sha
×
129
                                        ? `${manifest.version}-${manifest.sha}`
130
                                        : manifest.version;
NEW
131
                                await this.update(manifest, current, updated, force, channel);
×
132
                        }
133

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

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

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

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

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

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

178
                const { owner, repo } = this.githubConfig;
27✔
179

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

188
                        const versionIndex = {};
27✔
189

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

194
                                // Find the appropriate asset for this platform/arch
195
                                const assetName = this.determineAssetName(version);
27✔
196
                                const asset = release.assets.find((a) => a.name === assetName);
117✔
197

198
                                if (asset) {
27!
199
                                        versionIndex[version] = asset.browser_download_url;
27✔
200
                                }
201
                        }
202

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

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

218
                const { owner, repo } = this.githubConfig;
27✔
219

220
                try {
27✔
221
                        let release;
222

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

240
                        const version = release.tag_name.replace(/^v/u, '');
27✔
241
                        const assetName = this.determineAssetName(version);
27✔
242
                        const asset = release.assets.find((a) => a.name === assetName);
153✔
243

244
                        if (!asset) {
27✔
245
                                const config = this.config;
9✔
246
                                throw new Error(
9✔
247
                                        `No suitable asset found for ${config.platform}-${config.arch} in release ${release.tag_name}`
248
                                );
249
                        }
250

251
                        // Return a basic manifest with the asset URL
252
                        return {
18✔
253
                                gz: asset.browser_download_url,
254
                                version,
255
                        };
256
                } catch (error) {
257
                        const statusCode = error.status;
9✔
258
                        if (statusCode === 404) {
9!
NEW
259
                                throw new Error(
×
260
                                        `Release not found for channel "${channel}" in ${owner}/${repo}`
261
                                );
262
                        }
263

264
                        throw error;
9✔
265
                }
266
        }
267

268
        // GitHub-specific version manifest fetching
269
        async fetchGitHubVersionManifest(version, url) {
270
                await this.ensureOctokit();
27✔
271
                ux.action.status = 'fetching version manifest from GitHub';
27✔
272

273
                const { owner, repo } = this.githubConfig;
27✔
274

275
                try {
27✔
276
                        debug(`Fetching release v${version} for ${owner}/${repo}`);
27✔
277
                        const { data: release } = await this.octokit.repos.getReleaseByTag({
27✔
278
                                owner,
279
                                repo,
280
                                tag: `v${version}`,
281
                        });
282

283
                        const assetName = this.determineAssetName(version);
18✔
284
                        const asset = release.assets.find((a) => a.name === assetName);
135✔
285

286
                        if (asset) {
18✔
287
                                // Use the asset URL from the release
288
                                return {
9✔
289
                                        gz: asset.browser_download_url,
290
                                        version,
291
                                };
292
                        }
293

294
                        // Fallback to the provided URL
295
                        return {
9✔
296
                                gz: url,
297
                                version,
298
                        };
299
                } catch (error) {
300
                        debug('Failed to fetch version manifest', error);
9✔
301
                        // Return a basic manifest using the URL we have
302
                        return {
9✔
303
                                gz: url,
304
                                version,
305
                        };
306
                }
307
        }
308

309
        determineAssetName(version) {
310
                const config = this.config;
135✔
311
                const platform = config.platform === 'wsl' ? 'linux' : config.platform;
135✔
312
                const ext = config.windows ? 'tar.gz' : 'tar.gz';
135!
313
                return `${config.bin}-v${version}-${platform}-${config.arch}.${ext}`;
135✔
314
        }
315

316
        getGitHubConfig() {
317
                const oclifConfig = this.config;
324✔
318
                const config = checkGitHubConfig(oclifConfig);
324✔
319
                if (!config) {
324✔
320
                        throw new Error(
36✔
321
                                'GitHub repository not configured. Add "oclif.update.github" with "owner" and "repo" fields to package.json'
322
                        );
323
                }
324

325
                return config;
288✔
326
        }
327
}
328

329
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