@camunda8/cli 4.1.0-alpha.1 → 4.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AGENTS.md — `element-template` plugin
|
|
2
2
|
|
|
3
3
|
Read [docs/design.md](./docs/design.md) before changing this plugin. It
|
|
4
|
-
covers the **why**: vendor bundle,
|
|
4
|
+
covers the **why**: vendor bundle, release-bundle source choice, cache
|
|
5
5
|
strategy (`upstreamRef`-keyed dedup), lazy bootstrap, and semver-based
|
|
6
6
|
version resolution.
|
|
7
7
|
|
|
@@ -12,7 +12,8 @@ version resolution.
|
|
|
12
12
|
| `c8ctl-plugin.ts` | Plugin API (metadata + commands export), subcommand dispatch table |
|
|
13
13
|
| `commands/<name>.ts` | One file per subcommand: `apply`, `edit`, `get`, `get-properties`, `info`, `search`, `sync` |
|
|
14
14
|
| `template-ref.ts` | `parseTemplateRef`, `readBpmnInput`, `getExecutionPlatformVersion`, `resolveOotbTemplate`, `loadTemplate` |
|
|
15
|
-
| `
|
|
15
|
+
| `cache.ts` | Cache I/O, sync, search, version resolution |
|
|
16
|
+
| `releases.ts` | `camunda/connectors` GitHub releases: listing, newest-release-per-minor selection, bundle download, tar.gz reader |
|
|
16
17
|
| `helpers.ts` | `--set` parsing, file/URL fetch, glob → regex, multi-binding lookup, condition warnings, `atomicOverwriteFile` |
|
|
17
18
|
| `binding.ts` | Binding-target resolution shared by `apply`/`edit` — which moddle child + property a `zeebe:input`/etc. binding writes to. Hand-rolled stand-in for bpmn-js-element-templates' internal `setPropertyValue`; see the note at the top of the file about replacing it once bpmn-io/bpmn-js-element-templates#236 ships. |
|
|
18
19
|
| `vendor.ts` | `resolveVendorBundle()` + the minimal bpmn-js `Modeler`/`modeling` type surface shared by `apply`/`edit` |
|
|
@@ -29,13 +30,13 @@ version resolution.
|
|
|
29
30
|
without reason — `metadata.upstreamRef` is the dedup key for
|
|
30
31
|
incremental sync.
|
|
31
32
|
- **No subcommand auto-bootstraps the cache.** OOTB-id resolution
|
|
32
|
-
is guarded by `requireCachePresent()` in `
|
|
33
|
+
is guarded by `requireCachePresent()` in `cache.ts`, which
|
|
33
34
|
throws the shared `CACHE_NOT_FOUND_MESSAGE` when the cache is
|
|
34
35
|
absent. The reason auto-bootstrap is forbidden: bootstrap progress
|
|
35
36
|
goes through `logger.info`, which writes to stdout in text mode —
|
|
36
37
|
it would corrupt `apply | bpmn lint` and `get <id> > template.json`
|
|
37
|
-
pipelines, and racing cold-cache invocations would both
|
|
38
|
-
same
|
|
38
|
+
pipelines, and racing cold-cache invocations would both download the
|
|
39
|
+
same bundles. Don't re-add a bootstrap call to any subcommand
|
|
39
40
|
without changing the logger story first.
|
|
40
41
|
- **Path/URL apply paths must not trigger the cache check.**
|
|
41
42
|
Detection happens in `parseTemplateRef()` in `template-ref.ts`
|
|
@@ -45,7 +46,7 @@ version resolution.
|
|
|
45
46
|
overwrites a user-owned file (cache or BPMN) must follow the same
|
|
46
47
|
pattern — a kill mid-write must not leave a truncated file.
|
|
47
48
|
- **`syncTemplates` is serialised by an advisory lockfile.** The
|
|
48
|
-
helper `withSyncLock` in `
|
|
49
|
+
helper `withSyncLock` in `cache.ts` holds
|
|
49
50
|
`<cacheDir>/.sync.lock` while the body runs, with stale-lock
|
|
50
51
|
recovery (dead PID or > 60 min old) and signal handlers
|
|
51
52
|
(SIGINT/SIGTERM/SIGHUP) that release before re-raising. Don't
|
|
@@ -79,9 +80,16 @@ version resolution.
|
|
|
79
80
|
WeakMap-based `sourceByDetail` side table in `loadTemplate` preserves
|
|
80
81
|
this identity so `get-properties` doesn't collapse them and `--set`
|
|
81
82
|
writes to all matching duplicates.
|
|
82
|
-
- **
|
|
83
|
-
`
|
|
84
|
-
|
|
83
|
+
- **Templates come from the `camunda/connectors` GitHub releases, not
|
|
84
|
+
`raw.githubusercontent.com`** (blocked in many enterprise networks —
|
|
85
|
+
c8ctl#530). `sync` keeps the newest release of each of the 4 newest
|
|
86
|
+
minor lines (`MAX_MINOR_LINES`), skipping drafts, release candidates
|
|
87
|
+
and releases whose bundle asset isn't published yet. A bundle URL
|
|
88
|
+
changes with every patch, so `--prune` is skipped whenever a download
|
|
89
|
+
failed — otherwise one transient error would drop a whole minor line.
|
|
90
|
+
The release listing endpoint is overridable via
|
|
91
|
+
`C8CTL_CONNECTORS_RELEASES_URL` — useful for tests against a local
|
|
92
|
+
fixture server.
|
|
85
93
|
|
|
86
94
|
## Testing
|
|
87
95
|
|
|
@@ -89,7 +97,8 @@ version resolution.
|
|
|
89
97
|
npm run build:vendor && node --experimental-strip-types --test tests/unit/element-template.test.ts
|
|
90
98
|
```
|
|
91
99
|
|
|
92
|
-
Smoke-test against the live
|
|
100
|
+
Smoke-test against the live connectors releases (writes to a throwaway
|
|
101
|
+
dir):
|
|
93
102
|
|
|
94
103
|
```bash
|
|
95
104
|
C8CTL_DATA_DIR=/tmp/c8ctl-smoke node --experimental-strip-types src/index.ts \
|
|
@@ -98,7 +98,7 @@ c8ctl element-template get io.camunda.connectors.HttpJson.v2 --no-icon # drop t
|
|
|
98
98
|
|
|
99
99
|
# Refresh the local OOTB template cache
|
|
100
100
|
c8ctl element-template sync
|
|
101
|
-
c8ctl element-template sync --prune # also drop entries no longer in
|
|
101
|
+
c8ctl element-template sync --prune # also drop entries no longer in a selected release
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
## Inspecting a template
|
|
@@ -302,10 +302,14 @@ warning at the end (the property won't be applied).
|
|
|
302
302
|
|
|
303
303
|
## How OOTB templates are resolved
|
|
304
304
|
|
|
305
|
-
1. **
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
1. **Source**: the `camunda/connectors` GitHub releases. `sync` lists
|
|
306
|
+
the releases, keeps the newest release of each of the 4 newest minor
|
|
307
|
+
lines (8.8.x, 8.9.x, ... — alphas count, release candidates don't),
|
|
308
|
+
and downloads
|
|
309
|
+
each one's `connectors-bundle-templates-<tag>.tar.gz` asset from
|
|
310
|
+
`github.com`. `raw.githubusercontent.com` is deliberately not used:
|
|
311
|
+
it is blocked in many enterprise networks. Override the release
|
|
312
|
+
listing endpoint via `C8CTL_CONNECTORS_RELEASES_URL` for testing.
|
|
309
313
|
2. **Populate the cache once** via `c8ctl element-template sync`.
|
|
310
314
|
Subcommands that resolve OOTB ids (`search`, `info`, `get-properties`,
|
|
311
315
|
`apply`, `get`) exit non-zero with a hint to run `sync` when the
|
|
@@ -314,14 +318,14 @@ warning at the end (the property won't be applied).
|
|
|
314
318
|
`apply | bpmn lint` or `get <id> > template.json`.
|
|
315
319
|
3. **Local file or URL** template args (paths containing `/` or `\`,
|
|
316
320
|
starting with `.`, ending in `.json`, or starting with `http(s)://`)
|
|
317
|
-
skip the
|
|
321
|
+
skip the cache entirely.
|
|
318
322
|
4. **Version selection** uses `semver.satisfies` against the BPMN's
|
|
319
323
|
`modeler:executionPlatformVersion` and each template's
|
|
320
324
|
`engines.camunda` constraint. Without `@<version>`, the highest
|
|
321
325
|
compatible version wins.
|
|
322
326
|
5. **Stale cache** (>7 days) prints a hint to run `sync`. No automatic
|
|
323
|
-
refresh — `sync` only
|
|
324
|
-
|
|
327
|
+
refresh — `sync` only downloads bundles not already cached (release
|
|
328
|
+
assets are tag-pinned, so incremental sync is free).
|
|
325
329
|
6. **Concurrent syncs** are serialised by an advisory lock at
|
|
326
330
|
`<cache-dir>/.sync.lock`. A second `sync` started while one is
|
|
327
331
|
already running exits non-zero rather than racing.
|
|
@@ -339,5 +343,5 @@ Set `C8CTL_DATA_DIR` to override.
|
|
|
339
343
|
## Design
|
|
340
344
|
|
|
341
345
|
See [`docs/design.md`](./docs/design.md) for the full reasoning behind
|
|
342
|
-
the
|
|
346
|
+
the release-bundle source choice, the vendor bundle, the cache strategy
|
|
343
347
|
(mirrored from Desktop Modeler), and version resolution.
|
|
@@ -8610,8 +8610,8 @@ function parseTemplateJson(content) {
|
|
|
8610
8610
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8611
8611
|
import { resolve as resolvePath2 } from "node:path";
|
|
8612
8612
|
|
|
8613
|
-
// default-plugins/element-template/
|
|
8614
|
-
var
|
|
8613
|
+
// default-plugins/element-template/cache.ts
|
|
8614
|
+
var import_semver3 = __toESM(require_semver2(), 1);
|
|
8615
8615
|
import {
|
|
8616
8616
|
closeSync,
|
|
8617
8617
|
existsSync as existsSync2,
|
|
@@ -8624,14 +8624,140 @@ import {
|
|
|
8624
8624
|
writeSync
|
|
8625
8625
|
} from "node:fs";
|
|
8626
8626
|
import { join } from "node:path";
|
|
8627
|
-
|
|
8628
|
-
|
|
8627
|
+
|
|
8628
|
+
// default-plugins/element-template/releases.ts
|
|
8629
|
+
var import_semver2 = __toESM(require_semver2(), 1);
|
|
8630
|
+
import { gunzipSync } from "node:zlib";
|
|
8631
|
+
var DEFAULT_RELEASES_URL = "https://api.github.com/repos/camunda/connectors/releases?per_page=100";
|
|
8632
|
+
var TEMPLATES_ASSET_PREFIX = "connectors-bundle-templates-";
|
|
8633
|
+
var TEMPLATES_ASSET_SUFFIX = ".tar.gz";
|
|
8634
|
+
var RELEASES_FETCH_TIMEOUT_MS = 3e4;
|
|
8635
|
+
var ASSET_FETCH_TIMEOUT_MS = 12e4;
|
|
8636
|
+
var MAX_BUNDLE_BYTES = 128 * 1024 * 1024;
|
|
8637
|
+
var MAX_MINOR_LINES = 4;
|
|
8638
|
+
function getReleasesUrl() {
|
|
8639
|
+
return process.env.C8CTL_CONNECTORS_RELEASES_URL || DEFAULT_RELEASES_URL;
|
|
8640
|
+
}
|
|
8641
|
+
function isReleaseCandidate(version) {
|
|
8642
|
+
const prerelease = import_semver2.default.prerelease(version);
|
|
8643
|
+
if (!prerelease) return false;
|
|
8644
|
+
return prerelease.flatMap((part) => typeof part === "string" ? part.split("-") : []).some((part) => /^rc\d*$/i.test(part));
|
|
8645
|
+
}
|
|
8646
|
+
function findTemplatesAssetUrl(release) {
|
|
8647
|
+
if (!Array.isArray(release.assets)) return null;
|
|
8648
|
+
for (const asset of release.assets) {
|
|
8649
|
+
if (!isRecord(asset)) continue;
|
|
8650
|
+
const { name: name2, browser_download_url: url } = asset;
|
|
8651
|
+
if (typeof name2 !== "string" || typeof url !== "string") continue;
|
|
8652
|
+
if (name2.startsWith(TEMPLATES_ASSET_PREFIX) && name2.endsWith(TEMPLATES_ASSET_SUFFIX)) {
|
|
8653
|
+
return url;
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
return null;
|
|
8657
|
+
}
|
|
8658
|
+
function parseReleases(raw) {
|
|
8659
|
+
if (!Array.isArray(raw)) {
|
|
8660
|
+
throw new Error("Connector releases response is not a JSON array");
|
|
8661
|
+
}
|
|
8662
|
+
const releases = [];
|
|
8663
|
+
for (const entry of raw) {
|
|
8664
|
+
if (!isRecord(entry)) continue;
|
|
8665
|
+
if (entry.draft === true) continue;
|
|
8666
|
+
const tag = entry.tag_name;
|
|
8667
|
+
if (typeof tag !== "string") continue;
|
|
8668
|
+
const version = import_semver2.default.valid(tag);
|
|
8669
|
+
if (!version || isReleaseCandidate(version)) continue;
|
|
8670
|
+
const assetUrl = findTemplatesAssetUrl(entry);
|
|
8671
|
+
if (!assetUrl) continue;
|
|
8672
|
+
releases.push({ tag, version, assetUrl });
|
|
8673
|
+
}
|
|
8674
|
+
return releases;
|
|
8675
|
+
}
|
|
8676
|
+
function selectLatestPerMinor(releases) {
|
|
8677
|
+
const latest = /* @__PURE__ */ new Map();
|
|
8678
|
+
for (const release of releases) {
|
|
8679
|
+
const key = `${import_semver2.default.major(release.version)}.${import_semver2.default.minor(release.version)}`;
|
|
8680
|
+
const current = latest.get(key);
|
|
8681
|
+
if (!current || import_semver2.default.gt(release.version, current.version)) {
|
|
8682
|
+
latest.set(key, release);
|
|
8683
|
+
}
|
|
8684
|
+
}
|
|
8685
|
+
return [...latest.values()].sort((a, b) => import_semver2.default.rcompare(a.version, b.version)).slice(0, MAX_MINOR_LINES);
|
|
8686
|
+
}
|
|
8687
|
+
async function fetchConnectorReleases() {
|
|
8688
|
+
const url = getReleasesUrl();
|
|
8689
|
+
const response = await fetch(url, {
|
|
8690
|
+
headers: {
|
|
8691
|
+
"User-Agent": USER_AGENT,
|
|
8692
|
+
Accept: "application/vnd.github+json"
|
|
8693
|
+
},
|
|
8694
|
+
signal: AbortSignal.timeout(RELEASES_FETCH_TIMEOUT_MS)
|
|
8695
|
+
});
|
|
8696
|
+
if (!response.ok) {
|
|
8697
|
+
const hint = response.status === 403 || response.status === 429 ? "\nThe GitHub API rate limit may be exhausted \u2014 retry later, or point C8CTL_CONNECTORS_RELEASES_URL at a mirror of the release listing." : "";
|
|
8698
|
+
throw new Error(
|
|
8699
|
+
`HTTP ${response.status} ${response.statusText} for ${url}${hint}`
|
|
8700
|
+
);
|
|
8701
|
+
}
|
|
8702
|
+
return selectLatestPerMinor(parseReleases(await response.json()));
|
|
8703
|
+
}
|
|
8704
|
+
async function fetchReleaseAsset(url) {
|
|
8705
|
+
const response = await fetch(url, {
|
|
8706
|
+
headers: { "User-Agent": USER_AGENT },
|
|
8707
|
+
signal: AbortSignal.timeout(ASSET_FETCH_TIMEOUT_MS)
|
|
8708
|
+
});
|
|
8709
|
+
if (!response.ok) {
|
|
8710
|
+
throw new Error(
|
|
8711
|
+
`HTTP ${response.status} ${response.statusText} for ${url}`
|
|
8712
|
+
);
|
|
8713
|
+
}
|
|
8714
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
8715
|
+
}
|
|
8716
|
+
var TAR_BLOCK_SIZE = 512;
|
|
8717
|
+
function readTarString(tar, offset, length) {
|
|
8718
|
+
const raw = tar.toString("utf-8", offset, offset + length);
|
|
8719
|
+
const nul = raw.indexOf("\0");
|
|
8720
|
+
return nul === -1 ? raw : raw.slice(0, nul);
|
|
8721
|
+
}
|
|
8722
|
+
function extractJsonEntries(gzipped) {
|
|
8723
|
+
const tar = gunzipSync(gzipped, { maxOutputLength: MAX_BUNDLE_BYTES });
|
|
8724
|
+
const entries = [];
|
|
8725
|
+
let offset = 0;
|
|
8726
|
+
while (offset + TAR_BLOCK_SIZE <= tar.length) {
|
|
8727
|
+
if (tar.subarray(offset, offset + TAR_BLOCK_SIZE).every((b) => b === 0)) {
|
|
8728
|
+
break;
|
|
8729
|
+
}
|
|
8730
|
+
const name2 = readTarString(tar, offset, 100);
|
|
8731
|
+
const prefix2 = readTarString(tar, offset + 345, 155);
|
|
8732
|
+
const sizeField = readTarString(tar, offset + 124, 12).trim();
|
|
8733
|
+
const size = Number.parseInt(sizeField, 8);
|
|
8734
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
8735
|
+
throw new Error(
|
|
8736
|
+
`Malformed tar header (bad size field) at byte ${offset}`
|
|
8737
|
+
);
|
|
8738
|
+
}
|
|
8739
|
+
const typeFlag = String.fromCharCode(tar[offset + 156]);
|
|
8740
|
+
const dataStart = offset + TAR_BLOCK_SIZE;
|
|
8741
|
+
const dataEnd = dataStart + size;
|
|
8742
|
+
if (dataEnd > tar.length) {
|
|
8743
|
+
throw new Error(`Truncated tar entry '${name2}' at byte ${offset}`);
|
|
8744
|
+
}
|
|
8745
|
+
const fullName = prefix2 ? `${prefix2}/${name2}` : name2;
|
|
8746
|
+
if ((typeFlag === "0" || typeFlag === "\0") && fullName.endsWith(".json")) {
|
|
8747
|
+
entries.push({
|
|
8748
|
+
name: fullName,
|
|
8749
|
+
content: tar.toString("utf-8", dataStart, dataEnd)
|
|
8750
|
+
});
|
|
8751
|
+
}
|
|
8752
|
+
offset = dataStart + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
|
8753
|
+
}
|
|
8754
|
+
return entries;
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
// default-plugins/element-template/cache.ts
|
|
8758
|
+
var FETCH_CONCURRENCY = 4;
|
|
8629
8759
|
var STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8630
|
-
var FETCH_TIMEOUT_MS = 3e4;
|
|
8631
8760
|
var SYNC_LOCK_STALE_AFTER_MS = 60 * 60 * 1e3;
|
|
8632
|
-
function getMarketplaceUrl() {
|
|
8633
|
-
return process.env.C8CTL_OOTB_ELEMENT_TEMPLATES_URL || DEFAULT_OOTB_URL;
|
|
8634
|
-
}
|
|
8635
8761
|
function getCacheDir() {
|
|
8636
8762
|
if (!globalThis.c8ctl?.getUserDataDir) {
|
|
8637
8763
|
throw new Error(
|
|
@@ -8796,7 +8922,7 @@ function atomicWriteFileSync(target, contents) {
|
|
|
8796
8922
|
throw error3;
|
|
8797
8923
|
}
|
|
8798
8924
|
}
|
|
8799
|
-
function saveCache(templates) {
|
|
8925
|
+
function saveCache(templates, { stampFetchedAt = true } = {}) {
|
|
8800
8926
|
const dir = getCacheDir();
|
|
8801
8927
|
mkdirSync(dir, { recursive: true });
|
|
8802
8928
|
atomicWriteFileSync(
|
|
@@ -8804,7 +8930,9 @@ function saveCache(templates) {
|
|
|
8804
8930
|
`${JSON.stringify(templates, null, 2)}
|
|
8805
8931
|
`
|
|
8806
8932
|
);
|
|
8807
|
-
|
|
8933
|
+
if (stampFetchedAt) {
|
|
8934
|
+
atomicWriteFileSync(getFetchedAtPath(), String(Date.now()));
|
|
8935
|
+
}
|
|
8808
8936
|
}
|
|
8809
8937
|
function isCacheStale() {
|
|
8810
8938
|
const fetchedAt = loadFetchedAt();
|
|
@@ -8825,50 +8953,41 @@ function nudgeIfStale(logger) {
|
|
|
8825
8953
|
`Element template cache is ${ageText}. Run 'c8ctl element-template sync' to refresh.`
|
|
8826
8954
|
);
|
|
8827
8955
|
}
|
|
8828
|
-
async function fetchJson(url) {
|
|
8829
|
-
const response = await fetch(url, {
|
|
8830
|
-
headers: { "User-Agent": USER_AGENT },
|
|
8831
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
8832
|
-
});
|
|
8833
|
-
if (!response.ok) {
|
|
8834
|
-
throw new Error(
|
|
8835
|
-
`HTTP ${response.status} ${response.statusText} for ${url}`
|
|
8836
|
-
);
|
|
8837
|
-
}
|
|
8838
|
-
return response.json();
|
|
8839
|
-
}
|
|
8840
|
-
async function fetchIndex() {
|
|
8841
|
-
const raw = await fetchJson(getMarketplaceUrl());
|
|
8842
|
-
if (!isRecord(raw)) {
|
|
8843
|
-
throw new Error("Marketplace index is not a JSON object");
|
|
8844
|
-
}
|
|
8845
|
-
const result = {};
|
|
8846
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
8847
|
-
if (!Array.isArray(value)) continue;
|
|
8848
|
-
const entries = [];
|
|
8849
|
-
for (const entry of value) {
|
|
8850
|
-
if (!isRecord(entry)) continue;
|
|
8851
|
-
entries.push({
|
|
8852
|
-
version: typeof entry.version === "number" ? entry.version : void 0,
|
|
8853
|
-
ref: typeof entry.ref === "string" ? entry.ref : void 0,
|
|
8854
|
-
engine: isRecord(entry.engine) ? {
|
|
8855
|
-
camunda: typeof entry.engine.camunda === "string" ? entry.engine.camunda : void 0
|
|
8856
|
-
} : void 0
|
|
8857
|
-
});
|
|
8858
|
-
}
|
|
8859
|
-
result[id] = entries;
|
|
8860
|
-
}
|
|
8861
|
-
return result;
|
|
8862
|
-
}
|
|
8863
8956
|
function isTemplateLike(value) {
|
|
8864
8957
|
return isRecord(value) && Array.isArray(value.properties);
|
|
8865
8958
|
}
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8959
|
+
function buildUpstreamRef(assetUrl, entryName) {
|
|
8960
|
+
return `${assetUrl}#${entryName}`;
|
|
8961
|
+
}
|
|
8962
|
+
function assetUrlOfRef(ref) {
|
|
8963
|
+
const hash = ref.indexOf("#");
|
|
8964
|
+
return hash === -1 ? ref : ref.slice(0, hash);
|
|
8965
|
+
}
|
|
8966
|
+
async function fetchReleaseTemplates(release) {
|
|
8967
|
+
const bundle = await fetchReleaseAsset(release.assetUrl);
|
|
8968
|
+
const templates = [];
|
|
8969
|
+
for (const entry of extractJsonEntries(bundle)) {
|
|
8970
|
+
let parsed;
|
|
8971
|
+
try {
|
|
8972
|
+
parsed = JSON.parse(entry.content);
|
|
8973
|
+
} catch {
|
|
8974
|
+
continue;
|
|
8975
|
+
}
|
|
8976
|
+
if (!isTemplateLike(parsed) || typeof parsed.version !== "number") {
|
|
8977
|
+
continue;
|
|
8978
|
+
}
|
|
8979
|
+
parsed.metadata = {
|
|
8980
|
+
...parsed.metadata,
|
|
8981
|
+
upstreamRef: buildUpstreamRef(release.assetUrl, entry.name)
|
|
8982
|
+
};
|
|
8983
|
+
templates.push(parsed);
|
|
8984
|
+
}
|
|
8985
|
+
if (templates.length === 0) {
|
|
8986
|
+
throw new Error(
|
|
8987
|
+
`Bundle for release ${release.tag} contained no element templates`
|
|
8988
|
+
);
|
|
8870
8989
|
}
|
|
8871
|
-
return
|
|
8990
|
+
return templates;
|
|
8872
8991
|
}
|
|
8873
8992
|
async function pool(items, concurrency, fn) {
|
|
8874
8993
|
const queue = items.slice();
|
|
@@ -8884,20 +9003,12 @@ async function pool(items, concurrency, fn) {
|
|
|
8884
9003
|
);
|
|
8885
9004
|
await Promise.all(workers);
|
|
8886
9005
|
}
|
|
8887
|
-
function
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
id,
|
|
8894
|
-
version: entry.version,
|
|
8895
|
-
ref: entry.ref,
|
|
8896
|
-
engine: entry.engine
|
|
8897
|
-
});
|
|
8898
|
-
}
|
|
8899
|
-
}
|
|
8900
|
-
return entries;
|
|
9006
|
+
function sortTemplates(templates) {
|
|
9007
|
+
return [...templates].sort((a, b) => {
|
|
9008
|
+
const idCmp = (a.id ?? "").localeCompare(b.id ?? "");
|
|
9009
|
+
if (idCmp !== 0) return idCmp;
|
|
9010
|
+
return (a.version ?? 0) - (b.version ?? 0);
|
|
9011
|
+
});
|
|
8901
9012
|
}
|
|
8902
9013
|
async function syncTemplates({
|
|
8903
9014
|
logger,
|
|
@@ -8909,11 +9020,16 @@ async function syncTemplatesLocked({
|
|
|
8909
9020
|
logger,
|
|
8910
9021
|
prune
|
|
8911
9022
|
}) {
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
const
|
|
9023
|
+
const releasesUrl = getReleasesUrl();
|
|
9024
|
+
logger.info(`Fetching connector releases from ${releasesUrl} ...`);
|
|
9025
|
+
const releases = await fetchConnectorReleases();
|
|
9026
|
+
if (releases.length === 0) {
|
|
9027
|
+
throw new Error(
|
|
9028
|
+
`No connector release with an element-template bundle found at ${releasesUrl}.`
|
|
9029
|
+
);
|
|
9030
|
+
}
|
|
8915
9031
|
logger.info(
|
|
8916
|
-
`
|
|
9032
|
+
`Latest release per minor: ${releases.map((r) => r.tag).join(", ")}.`
|
|
8917
9033
|
);
|
|
8918
9034
|
let existing;
|
|
8919
9035
|
try {
|
|
@@ -8923,70 +9039,90 @@ async function syncTemplatesLocked({
|
|
|
8923
9039
|
logger.warn(`Corrupt cache \u2014 starting fresh: ${message}`);
|
|
8924
9040
|
existing = [];
|
|
8925
9041
|
}
|
|
8926
|
-
const
|
|
9042
|
+
const selectedAssets = new Set(releases.map((r) => r.assetUrl));
|
|
9043
|
+
const cachedByAsset = /* @__PURE__ */ new Map();
|
|
9044
|
+
const staleCached = [];
|
|
8927
9045
|
for (const tpl of existing) {
|
|
8928
9046
|
const ref = tpl.metadata?.upstreamRef;
|
|
8929
|
-
|
|
9047
|
+
const asset = ref ? assetUrlOfRef(ref) : void 0;
|
|
9048
|
+
if (asset && selectedAssets.has(asset)) {
|
|
9049
|
+
const bucket = cachedByAsset.get(asset);
|
|
9050
|
+
if (bucket) bucket.push(tpl);
|
|
9051
|
+
else cachedByAsset.set(asset, [tpl]);
|
|
9052
|
+
} else {
|
|
9053
|
+
staleCached.push(tpl);
|
|
9054
|
+
}
|
|
8930
9055
|
}
|
|
8931
|
-
const
|
|
8932
|
-
const
|
|
9056
|
+
const toFetch = releases.filter((r) => !cachedByAsset.has(r.assetUrl));
|
|
9057
|
+
const reusedCount = [...cachedByAsset.values()].reduce(
|
|
9058
|
+
(sum, bucket) => sum + bucket.length,
|
|
9059
|
+
0
|
|
9060
|
+
);
|
|
8933
9061
|
logger.info(
|
|
8934
|
-
`${
|
|
9062
|
+
`${reusedCount > 0 ? `Reusing ${reusedCount} cached templates from ${cachedByAsset.size} bundle(s), ` : ""}downloading ${toFetch.length} bundle(s)...`
|
|
8935
9063
|
);
|
|
8936
|
-
let fetched = 0;
|
|
8937
9064
|
let errors = 0;
|
|
8938
9065
|
let progress = 0;
|
|
8939
|
-
const
|
|
8940
|
-
await pool(toFetch, FETCH_CONCURRENCY, async (
|
|
9066
|
+
const fetchedByAsset = /* @__PURE__ */ new Map();
|
|
9067
|
+
await pool(toFetch, FETCH_CONCURRENCY, async (release) => {
|
|
8941
9068
|
progress += 1;
|
|
8942
9069
|
const myProgress = progress;
|
|
8943
|
-
const label = `${entry.id}@${entry.version}`;
|
|
8944
9070
|
try {
|
|
8945
|
-
const
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
logger.info(` [${myProgress}/${toFetch.length}] ${label}`);
|
|
9071
|
+
const templates = await fetchReleaseTemplates(release);
|
|
9072
|
+
fetchedByAsset.set(release.assetUrl, templates);
|
|
9073
|
+
logger.info(
|
|
9074
|
+
` [${myProgress}/${toFetch.length}] ${release.tag} \u2014 ${templates.length} templates`
|
|
9075
|
+
);
|
|
8951
9076
|
} catch (error3) {
|
|
8952
9077
|
errors += 1;
|
|
8953
9078
|
const message = error3 instanceof Error ? error3.message : String(error3);
|
|
8954
|
-
logger.warn(
|
|
9079
|
+
logger.warn(
|
|
9080
|
+
` [${myProgress}/${toFetch.length}] ${release.tag} \u2014 ${message}`
|
|
9081
|
+
);
|
|
8955
9082
|
}
|
|
8956
9083
|
});
|
|
8957
|
-
fetchedTemplates.sort((a, b) => {
|
|
8958
|
-
const idCmp = (a.id ?? "").localeCompare(b.id ?? "");
|
|
8959
|
-
if (idCmp !== 0) return idCmp;
|
|
8960
|
-
return (a.version ?? 0) - (b.version ?? 0);
|
|
8961
|
-
});
|
|
8962
9084
|
const next = [];
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
9085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9086
|
+
let fetched = 0;
|
|
9087
|
+
let cached = 0;
|
|
9088
|
+
const add = (tpl) => {
|
|
9089
|
+
const key = `${tpl.id}@${tpl.version}`;
|
|
9090
|
+
if (seen.has(key)) return false;
|
|
9091
|
+
seen.add(key);
|
|
9092
|
+
next.push(tpl);
|
|
9093
|
+
return true;
|
|
9094
|
+
};
|
|
9095
|
+
for (const release of releases) {
|
|
9096
|
+
const fresh = fetchedByAsset.get(release.assetUrl);
|
|
9097
|
+
const templates = fresh ?? cachedByAsset.get(release.assetUrl) ?? [];
|
|
9098
|
+
for (const tpl of sortTemplates(templates)) {
|
|
9099
|
+
if (!add(tpl)) continue;
|
|
9100
|
+
if (fresh) fetched += 1;
|
|
9101
|
+
else cached += 1;
|
|
8967
9102
|
}
|
|
8968
9103
|
}
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
return ref === void 0 || !freshRefs.has(ref);
|
|
8975
|
-
}).length;
|
|
9104
|
+
const pruning = prune && errors === 0;
|
|
9105
|
+
if (prune && errors > 0) {
|
|
9106
|
+
logger.warn(
|
|
9107
|
+
`Skipping prune: ${errors} bundle(s) failed to download \u2014 cached templates were kept.`
|
|
9108
|
+
);
|
|
8976
9109
|
}
|
|
8977
|
-
if (!
|
|
8978
|
-
for (const tpl of
|
|
8979
|
-
|
|
8980
|
-
if (ref && !freshRefs.has(ref) && !next.includes(tpl)) {
|
|
8981
|
-
next.push(tpl);
|
|
8982
|
-
}
|
|
9110
|
+
if (!pruning) {
|
|
9111
|
+
for (const tpl of sortTemplates(staleCached)) {
|
|
9112
|
+
add(tpl);
|
|
8983
9113
|
}
|
|
8984
9114
|
}
|
|
8985
|
-
|
|
9115
|
+
const pruned = pruning ? staleCached.length : 0;
|
|
9116
|
+
if (next.length === 0) {
|
|
9117
|
+
throw new Error(
|
|
9118
|
+
`No element templates could be downloaded (${errors} bundle(s) failed). Check the warnings above and retry 'c8ctl element-template sync'.`
|
|
9119
|
+
);
|
|
9120
|
+
}
|
|
9121
|
+
saveCache(next, { stampFetchedAt: errors === 0 });
|
|
8986
9122
|
const summary = {
|
|
8987
9123
|
total: next.length,
|
|
8988
9124
|
fetched,
|
|
8989
|
-
cached
|
|
9125
|
+
cached,
|
|
8990
9126
|
errors,
|
|
8991
9127
|
pruned
|
|
8992
9128
|
};
|
|
@@ -9015,12 +9151,12 @@ function pickVersion(templates, { version, executionPlatformVersion } = {}) {
|
|
|
9015
9151
|
}
|
|
9016
9152
|
let candidates = templates.filter((t) => t.version !== void 0);
|
|
9017
9153
|
if (executionPlatformVersion) {
|
|
9018
|
-
const coerced =
|
|
9154
|
+
const coerced = import_semver3.default.coerce(executionPlatformVersion);
|
|
9019
9155
|
if (coerced) {
|
|
9020
9156
|
candidates = candidates.filter((t) => {
|
|
9021
9157
|
const constraint = t.engines?.camunda;
|
|
9022
9158
|
if (!constraint) return true;
|
|
9023
|
-
return
|
|
9159
|
+
return import_semver3.default.satisfies(coerced, constraint);
|
|
9024
9160
|
});
|
|
9025
9161
|
}
|
|
9026
9162
|
}
|
|
@@ -9040,12 +9176,12 @@ function searchTemplates(query, { executionPlatformVersion } = {}) {
|
|
|
9040
9176
|
return name2.includes(q) || description.includes(q) || id.includes(q) || keywords.includes(q);
|
|
9041
9177
|
}).filter((t) => !t.deprecated);
|
|
9042
9178
|
if (executionPlatformVersion) {
|
|
9043
|
-
const coerced =
|
|
9179
|
+
const coerced = import_semver3.default.coerce(executionPlatformVersion);
|
|
9044
9180
|
if (coerced) {
|
|
9045
9181
|
matches = matches.filter((t) => {
|
|
9046
9182
|
const constraint = t.engines?.camunda;
|
|
9047
9183
|
if (!constraint) return true;
|
|
9048
|
-
return
|
|
9184
|
+
return import_semver3.default.satisfies(coerced, constraint);
|
|
9049
9185
|
});
|
|
9050
9186
|
}
|
|
9051
9187
|
}
|
|
@@ -10463,7 +10599,7 @@ var commands = {
|
|
|
10463
10599
|
},
|
|
10464
10600
|
prune: {
|
|
10465
10601
|
type: "boolean",
|
|
10466
|
-
description: "Drop cached entries no longer in
|
|
10602
|
+
description: "Drop cached entries no longer in a selected release [sync]"
|
|
10467
10603
|
},
|
|
10468
10604
|
"no-icon": {
|
|
10469
10605
|
type: "boolean",
|
|
@@ -24,29 +24,61 @@ shipped layout but during development we run directly from
|
|
|
24
24
|
|
|
25
25
|
## OOTB template integration
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Four sources publish the OOTB connector templates:
|
|
28
28
|
|
|
29
29
|
| | Source-of-truth | Inlines templates? | Has all versions? | Used by |
|
|
30
30
|
|---|---|---|---|---|
|
|
31
|
+
| `github.com/camunda/connectors` releases (bundle asset) | yes | yes (~1 MB gzip per line) | per minor line | c8ctl |
|
|
31
32
|
| `marketplace.cloud.camunda.io/api/v1/ootb-connectors` | mirrors GH | no, URL refs | yes | Desktop Modeler |
|
|
32
33
|
| `github.com/camunda/connectors/connector-templates.json` | yes | no, URL refs | yes | (the marketplace) |
|
|
33
34
|
| `@camunda/connectors-element-templates` (npm) | derived | yes (~9MB) | yes, may lag | (Web Modeler skill) |
|
|
34
35
|
|
|
35
|
-
We
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- The endpoint
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
We use the **release bundle assets** — every connectors release ships a
|
|
37
|
+
`connectors-bundle-templates-<tag>.tar.gz` with one JSON file per
|
|
38
|
+
template:
|
|
39
|
+
|
|
40
|
+
- The marketplace endpoint (Desktop Modeler's source, and c8ctl's
|
|
41
|
+
original choice) only publishes *refs* into
|
|
42
|
+
`raw.githubusercontent.com`, a host routinely blocked in enterprise
|
|
43
|
+
networks — the reason for the switch, see c8ctl#530. Release
|
|
44
|
+
downloads are served from `github.com`, which those environments
|
|
45
|
+
generally do reach.
|
|
46
|
+
- One request per minor line replaces ~650 per-template requests, so a
|
|
47
|
+
cold sync is a few MB and a few seconds instead of ~14 MB and ~30 s.
|
|
42
48
|
- npm package was rejected: confirmed lag against the GH source and
|
|
43
49
|
some entries are missing `version`/`engines` (pre-versioned legacy
|
|
44
50
|
templates).
|
|
45
|
-
- GH directly was rejected because the marketplace gives us the same
|
|
46
|
-
content with one less reason to drift from Modeler's behavior.
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
### Which releases
|
|
53
|
+
|
|
54
|
+
`sync` lists the releases (`api.github.com/repos/camunda/connectors/
|
|
55
|
+
releases?per_page=100`, overridable via
|
|
56
|
+
`C8CTL_CONNECTORS_RELEASES_URL`) and keeps **the newest release of each
|
|
57
|
+
minor line** — 8.8.x, 8.9.x, 8.10.x, ...:
|
|
58
|
+
|
|
59
|
+
- Bundles are cumulative within a line, so the newest patch of 8.8
|
|
60
|
+
already contains everything 8.8.17 shipped. Across lines they differ,
|
|
61
|
+
which is why one release per line is kept rather than just the newest
|
|
62
|
+
release overall.
|
|
63
|
+
- Alphas count (`8.10.0-alpha3` is the only source for a line that has
|
|
64
|
+
no stable release yet); release candidates do not — they are
|
|
65
|
+
superseded within days. Note that semver ranks `8.10.0-alpha5-rc3`
|
|
66
|
+
*above* `8.10.0-alpha5`, so RCs are filtered explicitly rather than
|
|
67
|
+
by ordering.
|
|
68
|
+
- Draft releases and releases without a bundle asset (a release that is
|
|
69
|
+
still being built) are skipped, so an in-flight release falls back to
|
|
70
|
+
the previous one on that line.
|
|
71
|
+
- Only the **4 newest minor lines** are kept (`MAX_MINOR_LINES` in
|
|
72
|
+
`releases.ts`), roughly Camunda's supported-version window. This keeps
|
|
73
|
+
the selection deterministic: the newest release of each selected line
|
|
74
|
+
is always within the first page of the listing, whereas an EOL line's
|
|
75
|
+
newest release drifts down the listing until it falls off the page and
|
|
76
|
+
would silently vanish from the selection.
|
|
77
|
+
|
|
78
|
+
Bundles contain a superset of the marketplace index: the `-hybrid`
|
|
79
|
+
variants and a few templates the marketplace does not list are cached
|
|
80
|
+
too, and templates without a numeric `version` (pre-versioned legacy
|
|
81
|
+
entries that version resolution cannot rank) are skipped.
|
|
50
82
|
|
|
51
83
|
## Cache strategy
|
|
52
84
|
|
|
@@ -56,12 +88,17 @@ We mirror Desktop Modeler's approach
|
|
|
56
88
|
- Cache lives in `<userDataDir>/element-templates/`:
|
|
57
89
|
- `templates.json` — flat array of all template objects (matches
|
|
58
90
|
Modeler's `.camunda-connector-templates.json` shape).
|
|
59
|
-
- `fetched-at` — epoch ms of last
|
|
60
|
-
- Each cached template gets
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
91
|
+
- `fetched-at` — epoch ms of last sync.
|
|
92
|
+
- Each cached template gets
|
|
93
|
+
`metadata.upstreamRef = <assetUrl>#<file-in-bundle>` injected. Release
|
|
94
|
+
assets are tag-pinned and therefore immutable, so "asset URL already
|
|
95
|
+
in the cache" ⇒ "bundle already ingested" ⇒ no re-download.
|
|
96
|
+
- `id@version` is deduplicated across bundles, newest release wins.
|
|
97
|
+
- Per-release fetch failures are logged + counted, and never abort the
|
|
98
|
+
run — unless *every* bundle failed and nothing was reusable from the
|
|
99
|
+
cache, in which case `sync` errors out rather than writing an empty
|
|
100
|
+
cache. A partial sync also leaves `fetched-at` untouched, so the
|
|
101
|
+
staleness nudge keeps asking for a full refresh.
|
|
65
102
|
|
|
66
103
|
### Lifecycle
|
|
67
104
|
|
|
@@ -79,11 +116,16 @@ We mirror Desktop Modeler's approach
|
|
|
79
116
|
arg in `parseTemplateRef` before any cache call.
|
|
80
117
|
- **Stale cache** (>7 days since `fetched-at`): warn-only, suggesting
|
|
81
118
|
`c8ctl element-template sync`. We don't auto-refresh — surprise
|
|
82
|
-
network activity inside `apply` is undesirable and
|
|
83
|
-
|
|
84
|
-
- **`sync`** always re-fetches the
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
network activity inside `apply` is undesirable and a manual sync is
|
|
120
|
+
cheap.
|
|
121
|
+
- **`sync`** always re-fetches the release listing but only downloads
|
|
122
|
+
bundles that aren't cached yet. **`sync --prune`** drops cached
|
|
123
|
+
entries that no longer belong to a selected release (opt-in: a user
|
|
124
|
+
may keep a legacy line intentionally). Pruning is skipped when any
|
|
125
|
+
bundle failed to download: a bundle URL changes with every patch, so
|
|
126
|
+
the previous patch's cached templates are always "stale", and dropping
|
|
127
|
+
them while their replacement failed would delete a whole minor line
|
|
128
|
+
over one transient HTTP error.
|
|
87
129
|
- **Atomicity**: `sync` writes `templates.json` and `fetched-at`
|
|
88
130
|
via a sibling temp file + `renameSync`, so a kill mid-sync leaves
|
|
89
131
|
the previous cache intact. `apply --in-place` uses the same recipe
|
|
@@ -100,9 +142,9 @@ We mirror Desktop Modeler's approach
|
|
|
100
142
|
|
|
101
143
|
Earlier draft considered fetching only the requested `(id, version)`
|
|
102
144
|
on demand. Rejected because **search needs the template names**, which
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
means every template has to be read anyway. Bundles make this moot: the
|
|
146
|
+
whole line arrives in one request, so eager bulk fetch is both simpler
|
|
147
|
+
and faster than any lazy scheme.
|
|
106
148
|
|
|
107
149
|
## Version resolution
|
|
108
150
|
|