Skip to content

Commit 784d891

Browse files
authored
[feature]: allow to install scoped packages (#2943)
* allow to install scoped packages * optimize code * added documentation
1 parent ddebf84 commit 784d891

File tree

4 files changed

+122
-91
lines changed

4 files changed

+122
-91
lines changed

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ The main configuration is stored in `iobroker-data/iobroker.json`. Normally, the
102102
- [js-controller Host Messages](#js-controller-host-messages)
103103
- [Adapter Development](#adapter-development)
104104
- [Environment Variables](#environment-variables)
105+
- [Vendor Packages Workflow](#vendor-packages-workflow)
105106

106107
### Admin UI
107108
**Feature status:** stable
@@ -1318,6 +1319,74 @@ However, on upgrades of Node.js these get lost. If js-controller detects a Node.
13181319

13191320
In some scenarios, e.g. during development it may be useful to deactivate this feature. You can do so by settings the `IOB_NO_SETCAP` environment variable to `true`.
13201321

1322+
### Vendor Packages Workflow
1323+
Feature status: New in 7.0.0
1324+
1325+
This feature is only of interest for vendors which aim to provide a package which is published to a private package registry (e.g. GitHub Packages).
1326+
This may be desirable if the adapter is only relevant for a specific customer and/or contains business logic which needs to be kept secret.
1327+
1328+
In the following, information is provided how private packages can be installed into the ioBroker ecosystem.
1329+
The information is tested with GitHub packages. However, it should work in a similar fashion with other registries, like GitLab Packages.
1330+
1331+
#### Package Registry
1332+
You can use e.g. the GitHub package registry. Simply scope your adapter to your organization or personal scope by changing the package name in the `package.json`
1333+
and configuring the `publishConfig`:
1334+
1335+
```json
1336+
{
1337+
"name": "@org/vendorAdapter",
1338+
"publishConfig": {
1339+
"registry": "https://npm.pkg.github.com"
1340+
}
1341+
}
1342+
```
1343+
1344+
Note, that you need to configure `npm` to authenticate via your registry.
1345+
Find more information in the [documentation](https://docs.npmjs.com/cli/v9/configuring-npm/npmrc#auth-related-configuration).
1346+
1347+
Example `.npmrc` file (can be in your project or in the users home directory):
1348+
1349+
```
1350+
//npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
1351+
@org:registry=https://npm.pkg.github.com
1352+
```
1353+
1354+
Where `YOUR_TOKEN` is an access token which has the permissions to write packages.
1355+
1356+
If you then execute `npm publish`, the package will be published to your custom registry instead of the `npm` registry.
1357+
1358+
#### Vendor Repository
1359+
In your vendor-specific repository, each adapter can have a separate field called `packetName`.
1360+
This represents the real name of the npm packet. E.g.
1361+
1362+
```json
1363+
{
1364+
"vendorAdapter": {
1365+
"version": "1.0.0",
1366+
"name": "vendorAdapter",
1367+
"packetName": "@org/vendorAdapter"
1368+
}
1369+
}
1370+
```
1371+
1372+
The js-controller will alias the package name to the adapter name on installation.
1373+
This has one drawback, which is normally not relevant for vendor setups. You can not install the adapter via the `npm url` command, meaning no installation from GitHub or local tarballs.
1374+
1375+
#### Token setup
1376+
On the customers ioBroker host, create a `.npmrc` file inside of `/home/iobroker/`.
1377+
It should look like:
1378+
1379+
```
1380+
//npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
1381+
@org:registry=https://npm.pkg.github.com
1382+
```
1383+
1384+
Where `YOUR_TOKEN` is an access token which has the permissions to read packages.
1385+
A best practice working with multiple customers is, to create an organization for each customer instead of using your personal scope.
1386+
Hence, you can scope them to not have access to packages of other customers or your own.
1387+
1388+
Find more information in the [documentation](https://docs.npmjs.com/cli/v9/configuring-npm/npmrc#auth-related-configuration).
1389+
13211390
## Release cycle and Development process overview
13221391
The goal is to release an update for the js-controller roughly all 6 months (April/September). The main reasons for this are shorter iterations and fewer changes that can be problematic for the users (and getting fast feedback) and also trying to stay up-to-date with the dependencies.
13231392

packages/cli/src/lib/setup/setupInstall.ts

Lines changed: 42 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -148,29 +148,28 @@ export class Install {
148148
/**
149149
* Download given packet
150150
*
151-
* @param repoUrl
152-
* @param packetName
151+
* @param repoUrlOrRepo repository url or already the repository object
152+
* @param packetName name of the package to install
153153
* @param options options.stopDb will stop the db before upgrade ONLY use it for controller upgrade - db is gone afterwards, does not work with stoppedList
154-
* @param stoppedList
154+
* @param stoppedList list of stopped instances (as instance objects)
155155
*/
156156
async downloadPacket(
157-
repoUrl: string | undefined | Record<string, any>,
157+
repoUrlOrRepo: string | undefined | Record<string, any>,
158158
packetName: string,
159159
options?: CLIDownloadPacketOptions,
160160
stoppedList?: ioBroker.InstanceObject[],
161161
): Promise<DownloadPacketReturnObject> {
162-
let url;
163162
if (!options || typeof options !== 'object') {
164163
options = {};
165164
}
166165

167166
stoppedList = stoppedList || [];
168-
let sources: Record<string, any>;
167+
let sources: Record<string, ioBroker.RepositoryJsonAdapterContent>;
169168

170-
if (!repoUrl || !tools.isObject(repoUrl)) {
171-
sources = await getRepository({ repoName: repoUrl, objects: this.objects });
169+
if (!repoUrlOrRepo || !tools.isObject(repoUrlOrRepo)) {
170+
sources = await getRepository({ repoName: repoUrlOrRepo, objects: this.objects });
172171
} else {
173-
sources = repoUrl;
172+
sources = repoUrlOrRepo;
174173
}
175174

176175
if (options.stopDb && stoppedList.length) {
@@ -194,97 +193,54 @@ export class Install {
194193
version = '';
195194
}
196195
}
197-
options.packetName = packetName;
198196

199-
options.unsafePerm = sources[packetName]?.unsafePerm;
197+
const source = sources[packetName];
198+
199+
if (!source) {
200+
const errMessage = `Unknown packet name ${packetName}. Please install packages from outside the repository using "${tools.appNameLowerCase} url <url-or-package>"!`;
201+
console.error(`host.${hostname} ${errMessage}`);
202+
throw new IoBrokerError({
203+
code: EXIT_CODES.UNKNOWN_PACKET_NAME,
204+
message: errMessage,
205+
});
206+
}
207+
208+
options.packetName = packetName;
209+
options.unsafePerm = source.unsafePerm;
200210

201211
// Check if flag stopBeforeUpdate is true or on windows we stop because of issue #1436
202-
if ((sources[packetName]?.stopBeforeUpdate || osPlatform === 'win32') && !stoppedList.length) {
212+
if ((source.stopBeforeUpdate || osPlatform === 'win32') && !stoppedList.length) {
203213
stoppedList = await this._getInstancesOfAdapter(packetName);
204214
await this.enableInstances(stoppedList, false);
205215
}
206216

207-
// try to extract the information from local sources-dist.json
208-
if (!sources[packetName]) {
209-
try {
210-
const sourcesDist = fs.readJsonSync(`${tools.getControllerDir()}/conf/sources-dist.json`);
211-
sources[packetName] = sourcesDist[packetName];
212-
} catch {
213-
// OK
217+
if (options.stopDb) {
218+
if (this.objects.destroy) {
219+
await this.objects.destroy();
220+
console.log('Stopped Objects DB');
214221
}
215-
}
216-
217-
if (sources[packetName]) {
218-
url = sources[packetName].url;
219-
220-
if (
221-
url &&
222-
packetName === 'js-controller' &&
223-
fs.pathExistsSync(
224-
`${tools.getControllerDir()}/../../node_modules/${tools.appName.toLowerCase()}.js-controller`,
225-
)
226-
) {
227-
url = null;
222+
if (this.states.destroy) {
223+
await this.states.destroy();
224+
console.log('Stopped States DB');
228225
}
226+
}
229227

230-
if (!url && packetName !== 'example') {
231-
if (options.stopDb) {
232-
if (this.objects.destroy) {
233-
await this.objects.destroy();
234-
console.log('Stopped Objects DB');
235-
}
236-
if (this.states.destroy) {
237-
await this.states.destroy();
238-
console.log('Stopped States DB');
239-
}
240-
}
241-
242-
// Install node modules
243-
await this._npmInstallWithCheck(
244-
`${tools.appName.toLowerCase()}.${packetName}${version ? `@${version}` : ''}`,
245-
options,
246-
debug,
247-
);
228+
// vendor packages could be scoped and thus differ in the package name
229+
const npmPacketName = source.packetName
230+
? `${tools.appName.toLowerCase()}.${packetName}@npm:${source.packetName}`
231+
: `${tools.appName.toLowerCase()}.${packetName}`;
248232

249-
return { packetName, stoppedList };
250-
} else if (url && url.match(this.tarballRegex)) {
251-
if (options.stopDb) {
252-
if (this.objects.destroy) {
253-
await this.objects.destroy();
254-
console.log('Stopped Objects DB');
255-
}
256-
if (this.states.destroy) {
257-
await this.states.destroy();
258-
console.log('Stopped States DB');
259-
}
260-
}
233+
// Install node modules
234+
await this._npmInstallWithCheck(`${npmPacketName}${version ? `@${version}` : ''}`, options, debug);
261235

262-
// Install node modules
263-
await this._npmInstallWithCheck(url, options, debug);
264-
return { packetName, stoppedList };
265-
} else if (!url) {
266-
// Adapter
267-
console.warn(
268-
`host.${hostname} Adapter "${packetName}" can be updated only together with ${tools.appName.toLowerCase()}.js-controller`,
269-
);
270-
return { packetName, stoppedList };
271-
}
272-
}
273-
274-
console.error(
275-
`host.${hostname} Unknown packet name ${packetName}. Please install packages from outside the repository using "${tools.appNameLowerCase} url <url-or-package>"!`,
276-
);
277-
throw new IoBrokerError({
278-
code: EXIT_CODES.UNKNOWN_PACKET_NAME,
279-
message: `Unknown packetName ${packetName}. Please install packages from outside the repository using npm!`,
280-
});
236+
return { packetName, stoppedList };
281237
}
282238

283239
/**
284240
* Install npm module from url
285241
*
286-
* @param npmUrl
287-
* @param options
242+
* @param npmUrl parameter passed to `npm install <npmUrl>`
243+
* @param options additional packet download options
288244
* @param debug if debug output should be printed
289245
*/
290246
private async _npmInstallWithCheck(
@@ -337,8 +293,8 @@ export class Install {
337293

338294
try {
339295
return await this._npmInstall({ npmUrl, options, debug, isRetry: false });
340-
} catch (err) {
341-
console.error(`Could not install ${npmUrl}: ${err.message}`);
296+
} catch (e) {
297+
console.error(`Could not install ${npmUrl}: ${e.message}`);
342298
}
343299
}
344300

@@ -379,7 +335,7 @@ export class Install {
379335
const { npmUrl, debug, isRetry } = installOptions;
380336
let { options } = installOptions;
381337

382-
if (typeof options !== 'object') {
338+
if (!tools.isObject(options)) {
383339
options = {};
384340
}
385341

packages/cli/src/lib/setup/utils.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@ interface GetRepositoryOptions {
1616
*
1717
* @param options Repository specific options
1818
*/
19-
export async function getRepository(options: GetRepositoryOptions): Promise<Record<string, any>> {
19+
export async function getRepository(
20+
options: GetRepositoryOptions,
21+
): Promise<Record<string, ioBroker.RepositoryJsonAdapterContent>> {
2022
const { objects } = options;
2123
const { repoName } = options;
2224

2325
let repoNameOrArray: string | string[] | undefined = repoName;
2426
if (!repoName || repoName === 'auto') {
25-
const systemConfig = await objects.getObjectAsync('system.config');
27+
const systemConfig = await objects.getObject('system.config');
2628
repoNameOrArray = systemConfig!.common.activeRepo;
2729
}
2830

2931
const repoArr = !Array.isArray(repoNameOrArray) ? [repoNameOrArray!] : repoNameOrArray;
3032

31-
const systemRepos = (await objects.getObjectAsync('system.repositories'))!;
33+
const systemRepos = (await objects.getObject('system.repositories'))!;
3234

33-
const allSources = {};
35+
const allSources: Record<string, ioBroker.RepositoryJsonAdapterContent> = {};
3436
let changed = false;
3537
let anyFound = false;
3638
for (const repoUrl of repoArr) {
@@ -62,7 +64,7 @@ export async function getRepository(options: GetRepositoryOptions): Promise<Reco
6264
}
6365

6466
if (changed) {
65-
await objects.setObjectAsync('system.repositories', systemRepos);
67+
await objects.setObject('system.repositories', systemRepos);
6668
}
6769
}
6870

packages/types-dev/objects.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,10 @@ declare global {
998998
version: string;
999999
/** Array of blocked versions, each entry represents a semver range */
10001000
blockedVersions: string[];
1001+
/** If true the unsafe perm flag is needed on install */
1002+
unsafePerm?: boolean;
1003+
/** If given, the packet name differs from the adapter name, e.g. because it is a scoped package */
1004+
packetName?: string;
10011005

10021006
/** Other Adapter related properties, not important for this implementation */
10031007
[other: string]: unknown;

0 commit comments

Comments
 (0)