Skip to content

Add support for installing/updating plugins from git URLs #95

@taylortom

Description

@taylortom

This is the companion to cgkineo/adapt-authoring-adaptframework-extras#9.

The adapt-cli PR #238 adds support for installing plugins directly from HTTPS git URLs (e.g. `adapt install https://github.com/org/plugin.git#v2.0.0\`). The contentplugin module needs to support this path so that:

  1. Plugins can be installed/updated by passing a git URL through the authoring tool API
  2. Git-installed plugins are tracked correctly in the DB and can be reinstalled after a restart

Implementation

Prerequisite: adapt-cli PR #238 must be merged and released.

1. DB schema — `schema/contentplugin.schema.json`

Add an optional `gitUrl` property to persist the source URL for git-installed plugins (analogous to `isLocalInstall`):

"gitUrl": {
  "description": "The HTTPS git URL this plugin was installed from, if applicable",
  "type": "string"
}

2. `installPlugin` — `lib/ContentPluginModule.js`

The current flow passes `versionOrPath` to `processPluginFiles`, which assumes a semver string or a local file path. A git URL will cause `fs.readdir` to throw.

Add a git URL detection branch before calling `processPluginFiles`:

async installPlugin (pluginName, versionOrPath, options = {}) {
  const isGitUrl = /^https?:\/\//.test(versionOrPath)

  let name, version, sourcePath, isLocalInstall, gitUrl

  if (isGitUrl) {
    // versionOrPath is e.g. "https://github.com/org/plugin.git#v2.0.0"
    // Pass it directly to the CLI — adapt-cli PR #238 handles git URL installs
    name = pluginName
    sourcePath = null
    isLocalInstall = false
    gitUrl = versionOrPath.split('#')[0]
  } else {
    const pluginData = await this.findOne({ name: String(pluginName) }, { includeUpdateInfo: true, strict: false })
    ;({ name, version, sourcePath, isLocalInstall } = await this.processPluginFiles({ ...pluginData, sourcePath: versionOrPath }))
  }

  const existingPlugin = await this.findOne({ name }, { strict: false })
  if (existingPlugin && !options.force) {
    if (!isGitUrl && semver.lte(version, existingPlugin.version)) {
      throw this.app.errors.CONTENTPLUGIN_ALREADY_EXISTS.setData({ name: existingPlugin.name, version: existingPlugin.version })
    }
  }

  const pluginArg = isGitUrl ? versionOrPath : `${name}@${sourcePath ?? version}`
  const [data] = await this.framework.runCliCommand('installPlugins', { plugins: [pluginArg] })

  const dbData = { ...(await data.getInfo()), type: await data.getType(), isLocalInstall }
  if (isGitUrl) dbData.gitUrl = gitUrl

  const info = await this.insertOrUpdate(dbData)
  if (!data.isInstallSuccessful) {
    throw this.app.errors.CONTENTPLUGIN_CLI_INSTALL_FAILED.setData({ name })
  }
  if (!info.targetAttribute) {
    throw this.app.errors.CONTENTPLUGIN_ATTR_MISSING.setData({ name })
  }
  await this.processPluginSchemas(data)
  return info
}

Note: after the CLI installs a git plugin, `data.name` is resolved from the cloned `bower.json` — so even if `pluginName` was an empty string it will be populated. Ensure `data.getInfo()` is called after `data.name` is set (adapt-cli PR #238 does this correctly via `fetchGitSourceInfo`).

3. `getMissingPlugins` — `lib/ContentPluginModule.js`

Currently non-local plugins are reinstalled as `${name}@${version}`. Git-installed plugins must be reinstalled using their stored git URL instead:

// For non-local, non-git installs:
return `${p.name}@${p.version}`

// For git installs (new):
if (p.gitUrl) return p.gitUrl
return `${p.name}@${p.version}`

Replace the existing non-local branch with this check.

4. `syncPluginData` — `lib/ContentPluginModule.js`

After adapt-cli PR #238, `getPluginUpdateInfos()` returns Plugin objects with `gitUrl` set for git-installed plugins (read from `_gitUrl` in `.bower.json`). Persist this when syncing:

for (const i of await this.framework.runCliCommand('getPluginUpdateInfos')) {
  if (dbInfo[i.name]?.version !== i.matchedVersion) {
    const pluginInfo = {
      ...(await i.getInfo()),
      type: await i.getType(),
      isLocalInstall: i.isLocalSource
    }
    if (i.isGitSource) pluginInfo.gitUrl = i.gitUrl
    await this.insertOrUpdate(pluginInfo)
  }
}

5. `updatePlugin` — `lib/ContentPluginModule.js`

The CLI's `updatePlugins` command already re-clones git plugins from their stored URL (adapt-cli PR #238). No special branching is needed here, but ensure that after update the `gitUrl` is preserved in the DB. Since `syncPluginData` is called via `postUpdateHook` already, this should be handled by the fix in step 4.

6. `installHandler` — `lib/ContentPluginModule.js`

The existing handler reads `req.body.version` as the version/path. No changes needed — a git URL submitted as `version` will be handled by the updated `installPlugin` method above.

Tests

  • `tests/utils-processPluginFiles.spec.js` (if it exists) or inline: confirm that a git URL input does not reach `fs.readdir`
  • New spec or additions to `ContentPluginUtils.spec.js`: mock `framework.runCliCommand` and verify that `installPlugin` with a git URL passes the full URL string to `installPlugins`, stores `gitUrl` in the DB record, and skips the `semver.lte` guard
  • Cover `getMissingPlugins`: when a DB plugin has `gitUrl` set, the returned reinstall spec is the git URL, not `name@version`

Metadata

Metadata

Labels

enhancementNew or additional functionality

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions