openvibers/openvibe.shared Commits
Pin drift (WS-C task 5): scripts/pin-drift.js fails on an openvibe-contracts/-shared/-sdk/-publishing pin that is not a published tag or is more than one minor (or a major) behind the latest release; the reusable test workflow runs it from main over the whole repository (pin-drift: false skips); custom CIs run the same curl line
Changes
@@@ -20,6 +20,9 @@ name: test# contracts openvibe-contracts-check --service <contracts-service> --src <dir> for each directory# in contracts-src, when contracts-service is set: the code's capability and event use# against the release++# pin drift scripts/pin-drift.js from OpenVibe.Shared main over the whole repository: a pin of++# openvibe-contracts/-shared/-sdk/-publishing that is not a published tag, or more than one++# minor behind the latest release, fails (roadmap WS-C task 5; `pin-drift: false` skips)# extra `extra`, when set: any further commands, one per line (e.g. a budget check)on:workflow_call:@@@ -60,6 +63,10 @@ on:description: Further commands after the tests, one per line ("" skips).type: stringdefault: ''++ pin-drift:++ description: Fail on drifted openvibe-* library pins (OpenVibe.Shared scripts/pin-drift.js).++ type: boolean++ default: truepermissions:contents: read@@@ -109,6 +116,10 @@ jobs:run: |args=(); for d in $SRC; do args+=(--src "$d"); donenpx openvibe-contracts-check --service "$SERVICE" "${args[@]}"++ - name: pin drift++ if: ${{ inputs.pin-drift }}++ working-directory: .++ run: curl -fsSL https://raw.githubusercontent.com/OpenVibers/OpenVibe.Shared/main/scripts/pin-drift.js | node - "$GITHUB_WORKSPACE"- name: extraif: ${{ inputs.extra != '' }}run: ${{ inputs.extra }}nnew file mode 100644@@@ -0,0 +1,82 @@++#!/usr/bin/env node++'use strict';++/**++ * Pin drift (roadmap WS-C task 5): every OpenVibers repository pins the shared libraries by release tag++ * (`https://codeload.github.com/OpenVibers/<repo>/tar.gz/refs/tags/vX.Y.Z`). CI fails when a pin++ *++ * - names a tag that is not published in that repository (an unpublished pin), or++ * - is older than the latest release by more than one minor (0.x: 0.40 fails when 0.42 is out; a major++ * behind always fails).++ *++ * node scripts/pin-drift.js [dir] every package.json under dir (git-tracked), default .++ * curl -fsSL https://raw.githubusercontent.com/OpenVibers/OpenVibe.Shared/main/scripts/pin-drift.js | node - [dir]++ *++ * Checks openvibe-contracts, openvibe-shared, openvibe-sdk and openvibe-publishing in dependencies,++ * devDependencies, optionalDependencies and peerDependencies; ranges that are not tag URLs (a peer range++ * like ">=1.5.0") are not pins and are skipped. Tags come from `git ls-remote --tags` (no token needed).++ * Exit 0 = no drift, 1 = drift, 2 = could not check (network).++ */++const fs = require('fs');++const path = require('path');++const cp = require('child_process');++++const LIBS = { 'openvibe-contracts': 'OpenVibe.Contracts', 'openvibe-shared': 'OpenVibe.Shared', 'openvibe-sdk': 'OpenVibe.SDK', 'openvibe-publishing': 'OpenVibe.Publishing' };++const PIN = /OpenVibers\/(OpenVibe\.[A-Za-z]+)\/tar\.gz\/refs\/tags\/v(\d+)\.(\d+)\.(\d+)$/;++++const parse = (t) => { const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(t); return m ? m.slice(1).map(Number) : null; };++const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];++++/** A pin's verdict against the published tags. → { ok, reason } */++function judge(pin, published) {++ const tags = published.map(parse).filter(Boolean).sort(cmp);++ if (!tags.length) return { ok: false, reason: 'no published tags' };++ if (!tags.some((t) => cmp(t, pin) === 0)) return { ok: false, reason: `v${pin.join('.')} is not a published tag` };++ const latest = tags[tags.length - 1];++ if (pin[0] < latest[0]) return { ok: false, reason: `v${pin.join('.')} is a major behind v${latest.join('.')}` };++ if (pin[0] === latest[0] && latest[1] - pin[1] > 1) return { ok: false, reason: `v${pin.join('.')} is ${latest[1] - pin[1]} minors behind v${latest.join('.')}` };++ return { ok: true, reason: `latest v${latest.join('.')}` };++}++++function packageFiles(dir) {++ try {++ return cp.execSync("git ls-files -- '*package.json' 'package.json'", { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).split('\n').filter((f) => f && !f.includes('node_modules/')).map((f) => path.join(dir, f));++ } catch { return fs.existsSync(path.join(dir, 'package.json')) ? [path.join(dir, 'package.json')] : []; }++}++++function pinsIn(file) {++ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));++ const out = [];++ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {++ for (const [name, spec] of Object.entries(pkg[field] || {})) {++ if (!LIBS[name]) continue;++ const m = PIN.exec(String(spec));++ if (m && m[1] === LIBS[name]) out.push({ file, field, name, repo: m[1], pin: [Number(m[2]), Number(m[3]), Number(m[4])] });++ }++ }++ return out;++}++++function main(dir = '.', { tagsOf } = {}) {++ const cache = new Map();++ const tags = tagsOf || ((repo) => {++ if (!cache.has(repo)) {++ const raw = cp.execSync(`git ls-remote --tags https://github.com/OpenVibers/${repo}.git`, { encoding: 'utf8', timeout: 30000 });++ cache.set(repo, [...new Set(raw.split('\n').map((l) => (l.split('refs/tags/')[1] || '').replace(/\^\{\}$/, '')).filter(Boolean))]);++ }++ return cache.get(repo);++ });++ const results = [];++ for (const file of packageFiles(dir)) for (const p of pinsIn(file)) results.push({ ...p, ...judge(p.pin, tags(p.repo)) });++ return results;++}++++module.exports = { judge, pinsIn, main, LIBS };++++if (require.main === module || !module.parent) {++ let results;++ try { results = main(process.argv[2] || '.'); } catch (err) { console.error(`pin-drift: could not read tags (${err.message})`); process.exit(2); }++ for (const r of results) console.log(`${r.ok ? 'ok ' : 'FAIL'} ${path.relative(process.cwd(), r.file) || r.file} ${r.name} v${r.pin.join('.')}: ${r.reason}`);++ const bad = results.filter((r) => !r.ok);++ console.log(bad.length ? `pin-drift: ${bad.length} of ${results.length} pin(s) drifted` : `pin-drift: ${results.length} pin(s), no drift`);++ process.exit(bad.length ? 1 : 0);++}nnew file mode 100644@@@ -0,0 +1,25 @@++'use strict';++// scripts/pin-drift.js (WS-C task 5): unpublished pins and pins more than one minor (or a major) behind fail.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { judge, main } = require('../scripts/pin-drift');++++const tags = ['v0.40.0', 'v0.41.0', 'v0.41.1', 'v0.42.0'];++assert.strictEqual(judge([0, 42, 0], tags).ok, true);++assert.strictEqual(judge([0, 41, 1], tags).ok, true, 'one minor behind is allowed');++assert.match(judge([0, 40, 0], tags).reason, /2 minors behind v0\.42\.0/);++assert.match(judge([0, 42, 1], tags).reason, /not a published tag/);++assert.match(judge([1, 12, 0], ['v1.12.0', 'v2.0.0']).reason, /major behind/);++++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pin-drift-'));++const url = (repo, v) => `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${v}`;++fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({++ dependencies: { 'openvibe-contracts': url('OpenVibe.Contracts', 'v0.39.0'), 'openvibe-sdk': url('OpenVibe.SDK', 'v0.9.1'), express: '^4.0.0' },++ peerDependencies: { 'openvibe-shared': '>=1.5.0' },++}));++const out = main(dir, { tagsOf: (repo) => (repo === 'OpenVibe.Contracts' ? tags : ['v0.9.0', 'v0.9.1']) });++assert.deepStrictEqual(out.map((r) => [r.name, r.ok]), [['openvibe-contracts', false], ['openvibe-sdk', true]], 'ranges are not pins; other packages are ignored');++fs.rmSync(dir, { recursive: true, force: true });++console.log('pin-drift: all checks passed');
@@ -20,6 +20,9 @@ name: test
@@ -20,6 +20,9 @@ name: test
# contracts openvibe-contracts-check --service <contracts-service> --src <dir> for each directory
# contracts openvibe-contracts-check --service <contracts-service> --src <dir> for each directory
# in contracts-src, when contracts-service is set: the code's capability and event use
# in contracts-src, when contracts-service is set: the code's capability and event use
# against the release
# against the release
+# pin drift scripts/pin-drift.js from OpenVibe.Shared main over the whole repository: a pin of
+# openvibe-contracts/-shared/-sdk/-publishing that is not a published tag, or more than one
+# minor behind the latest release, fails (roadmap WS-C task 5; `pin-drift: false` skips)
# extra `extra`, when set: any further commands, one per line (e.g. a budget check)
# extra `extra`, when set: any further commands, one per line (e.g. a budget check)
on:
on:
workflow_call:
workflow_call:
@@ -60,6 +63,10 @@ on:
@@ -60,6 +63,10 @@ on:
description: Further commands after the tests, one per line ("" skips).
description: Further commands after the tests, one per line ("" skips).
type: string
type: string
default: ''
default: ''
+ pin-drift:
+ description: Fail on drifted openvibe-* library pins (OpenVibe.Shared scripts/pin-drift.js).
+ type: boolean
+ default: true
permissions:
permissions:
contents: read
contents: read
@@ -109,6 +116,10 @@ jobs:
@@ -109,6 +116,10 @@ jobs:
run: |
run: |
args=(); for d in $SRC; do args+=(--src "$d"); done
args=(); for d in $SRC; do args+=(--src "$d"); done
npx openvibe-contracts-check --service "$SERVICE" "${args[@]}"
npx openvibe-contracts-check --service "$SERVICE" "${args[@]}"
+ - name: pin drift
+ if: ${{ inputs.pin-drift }}
+ working-directory: .
+ run: curl -fsSL https://raw.githubusercontent.com/OpenVibers/OpenVibe.Shared/main/scripts/pin-drift.js | node - "$GITHUB_WORKSPACE"
- name: extra
- name: extra
if: ${{ inputs.extra != '' }}
if: ${{ inputs.extra != '' }}
run: ${{ inputs.extra }}
run: ${{ inputs.extra }}
new file mode 100644
new file mode 100644
@@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Pin drift (roadmap WS-C task 5): every OpenVibers repository pins the shared libraries by release tag
+ * (`https://codeload.github.com/OpenVibers/<repo>/tar.gz/refs/tags/vX.Y.Z`). CI fails when a pin
+ *
+ * - names a tag that is not published in that repository (an unpublished pin), or
+ * - is older than the latest release by more than one minor (0.x: 0.40 fails when 0.42 is out; a major
+ * behind always fails).
+ *
+ * node scripts/pin-drift.js [dir] every package.json under dir (git-tracked), default .
+ * curl -fsSL https://raw.githubusercontent.com/OpenVibers/OpenVibe.Shared/main/scripts/pin-drift.js | node - [dir]
+ *
+ * Checks openvibe-contracts, openvibe-shared, openvibe-sdk and openvibe-publishing in dependencies,
+ * devDependencies, optionalDependencies and peerDependencies; ranges that are not tag URLs (a peer range
+ * like ">=1.5.0") are not pins and are skipped. Tags come from `git ls-remote --tags` (no token needed).
+ * Exit 0 = no drift, 1 = drift, 2 = could not check (network).
+ */
+const fs = require('fs');
+const path = require('path');
+const cp = require('child_process');
+
+const LIBS = { 'openvibe-contracts': 'OpenVibe.Contracts', 'openvibe-shared': 'OpenVibe.Shared', 'openvibe-sdk': 'OpenVibe.SDK', 'openvibe-publishing': 'OpenVibe.Publishing' };
+const PIN = /OpenVibers\/(OpenVibe\.[A-Za-z]+)\/tar\.gz\/refs\/tags\/v(\d+)\.(\d+)\.(\d+)$/;
+
+const parse = (t) => { const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(t); return m ? m.slice(1).map(Number) : null; };
+const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
+
+/** A pin's verdict against the published tags. → { ok, reason } */
+function judge(pin, published) {
+ const tags = published.map(parse).filter(Boolean).sort(cmp);
+ if (!tags.length) return { ok: false, reason: 'no published tags' };
+ if (!tags.some((t) => cmp(t, pin) === 0)) return { ok: false, reason: `v${pin.join('.')} is not a published tag` };
+ const latest = tags[tags.length - 1];
+ if (pin[0] < latest[0]) return { ok: false, reason: `v${pin.join('.')} is a major behind v${latest.join('.')}` };
+ if (pin[0] === latest[0] && latest[1] - pin[1] > 1) return { ok: false, reason: `v${pin.join('.')} is ${latest[1] - pin[1]} minors behind v${latest.join('.')}` };
+ return { ok: true, reason: `latest v${latest.join('.')}` };
+}
+
+function packageFiles(dir) {
+ try {
+ return cp.execSync("git ls-files -- '*package.json' 'package.json'", { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).split('\n').filter((f) => f && !f.includes('node_modules/')).map((f) => path.join(dir, f));
+ } catch { return fs.existsSync(path.join(dir, 'package.json')) ? [path.join(dir, 'package.json')] : []; }
+}
+
+function pinsIn(file) {
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
+ const out = [];
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
+ for (const [name, spec] of Object.entries(pkg[field] || {})) {
+ if (!LIBS[name]) continue;
+ const m = PIN.exec(String(spec));
+ if (m && m[1] === LIBS[name]) out.push({ file, field, name, repo: m[1], pin: [Number(m[2]), Number(m[3]), Number(m[4])] });
+ }
+ }
+ return out;
+}
+
+function main(dir = '.', { tagsOf } = {}) {
+ const cache = new Map();
+ const tags = tagsOf || ((repo) => {
+ if (!cache.has(repo)) {
+ const raw = cp.execSync(`git ls-remote --tags https://github.com/OpenVibers/${repo}.git`, { encoding: 'utf8', timeout: 30000 });
+ cache.set(repo, [...new Set(raw.split('\n').map((l) => (l.split('refs/tags/')[1] || '').replace(/\^\{\}$/, '')).filter(Boolean))]);
+ }
+ return cache.get(repo);
+ });
+ const results = [];
+ for (const file of packageFiles(dir)) for (const p of pinsIn(file)) results.push({ ...p, ...judge(p.pin, tags(p.repo)) });
+ return results;
+}
+
+module.exports = { judge, pinsIn, main, LIBS };
+
+if (require.main === module || !module.parent) {
+ let results;
+ try { results = main(process.argv[2] || '.'); } catch (err) { console.error(`pin-drift: could not read tags (${err.message})`); process.exit(2); }
+ for (const r of results) console.log(`${r.ok ? 'ok ' : 'FAIL'} ${path.relative(process.cwd(), r.file) || r.file} ${r.name} v${r.pin.join('.')}: ${r.reason}`);
+ const bad = results.filter((r) => !r.ok);
+ console.log(bad.length ? `pin-drift: ${bad.length} of ${results.length} pin(s) drifted` : `pin-drift: ${results.length} pin(s), no drift`);
+ process.exit(bad.length ? 1 : 0);
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+'use strict';
+// scripts/pin-drift.js (WS-C task 5): unpublished pins and pins more than one minor (or a major) behind fail.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { judge, main } = require('../scripts/pin-drift');
+
+const tags = ['v0.40.0', 'v0.41.0', 'v0.41.1', 'v0.42.0'];
+assert.strictEqual(judge([0, 42, 0], tags).ok, true);
+assert.strictEqual(judge([0, 41, 1], tags).ok, true, 'one minor behind is allowed');
+assert.match(judge([0, 40, 0], tags).reason, /2 minors behind v0\.42\.0/);
+assert.match(judge([0, 42, 1], tags).reason, /not a published tag/);
+assert.match(judge([1, 12, 0], ['v1.12.0', 'v2.0.0']).reason, /major behind/);
+
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pin-drift-'));
+const url = (repo, v) => `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${v}`;
+fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
+ dependencies: { 'openvibe-contracts': url('OpenVibe.Contracts', 'v0.39.0'), 'openvibe-sdk': url('OpenVibe.SDK', 'v0.9.1'), express: '^4.0.0' },
+ peerDependencies: { 'openvibe-shared': '>=1.5.0' },
+}));
+const out = main(dir, { tagsOf: (repo) => (repo === 'OpenVibe.Contracts' ? tags : ['v0.9.0', 'v0.9.1']) });
+assert.deepStrictEqual(out.map((r) => [r.name, r.ok]), [['openvibe-contracts', false], ['openvibe-sdk', true]], 'ranges are not pins; other packages are ignored');
+fs.rmSync(dir, { recursive: true, force: true });
+console.log('pin-drift: all checks passed');