Merge branch 'ci' into 'master'

Add CI/CD

See merge request riftenlabs/lib/wizardconnect!1
This commit is contained in:
Dagur Valberg Johannsson 2026-03-06 14:16:50 +00:00
commit 8c02f72eee
8 changed files with 494 additions and 12 deletions

97
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,97 @@
.node-common:
image: node:trixie-slim
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm-cache"
cache:
key:
files:
- package-lock.json
paths:
- .npm-cache/
before_script:
- node --version
security_checker:
extends: .node-common
script:
- npm audit --audit-level=moderate
allow_failure: false
test:
extends: .node-common
script:
- npm install
- npm run test
build:
extends: .node-common
script:
- npm install
- npm run build
artifacts:
paths:
- packages/*/dist/
- node_modules/
expire_in: 1 hour
lint:
extends: .node-common
script:
- npm install
- npm run build # required by eslint import plugin
- npm run lint
test-integration:
extends: .node-common
script:
- npm install
- npm run build
- npm run test:integration --workspace=@wizardconnect/wallet
publish:
extends: .node-common
# npm trusted publishing via GitLab OIDC — no NPM_TOKEN needed.
# Each @wizardconnect/* package must be linked to this GitLab project
# on npmjs.com: package Settings > Publishing access > Trusted publishers.
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- apt-get update && apt-get install -y git
- node contrib/auto-publish.js
rules:
- if: $CI_COMMIT_BRANCH == "master"
needs:
- test
- lint
pages:
image: python:3-slim
cache:
key: pages-pip
paths:
- .pip-cache/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
script:
- pip install zensical
- zensical build
- mv site public
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == "master"
force-publish:
extends: .node-common
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- apt-get update && apt-get install -y git
- node contrib/force-publish.js
rules:
- if: $CI_COMMIT_BRANCH == "master"
when: manual
allow_failure: false

208
contrib/auto-publish.js Normal file
View file

@ -0,0 +1,208 @@
#!/usr/bin/env node
/**
* Fully automated package publishing for monorepo
* - Detects changes automatically
* - Gets latest version from npm and bumps appropriately
* - Skips private packages
* - Uses npm trusted publishing (OIDC provenance) — no NPM_TOKEN needed
* - No git commits, just publishes
* - No manual interaction required
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
function discoverPackages() {
const packagesDir = 'packages';
const packagePaths = {};
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const packageDir of packageDirs) {
const packageJsonPath = path.join(packagesDir, packageDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.private) {
log(`Skipping private package: ${packageJson.name || packageDir}`);
continue;
}
if (packageJson.name) {
packagePaths[packageJson.name] = path.join(packagesDir, packageDir);
}
} catch (error) {
log(`Warning: Could not parse package.json in ${packageDir}: ${error.message}`);
}
}
}
return packagePaths;
}
function log(message) {
console.log(`[AUTO-PUBLISH] ${message}`);
}
function execCommand(command, cwd = process.cwd()) {
try {
return execSync(command, { cwd, stdio: 'pipe', encoding: 'utf8' });
} catch (error) {
throw new Error(`Command failed: ${command}\n${error.message}`);
}
}
function checkPackageChanges(packageName, packagePath) {
log(`Checking changes for ${packageName} in ${packagePath}`);
const latestNpmVersion = getLatestVersionFromNpm(packageName);
if (latestNpmVersion === null) {
log(`New package ${packageName} detected - will be published`);
return true;
}
try {
execCommand(`git diff --quiet HEAD~1 HEAD -- ${packagePath}/src/`);
return false;
} catch (error) {
return true;
}
}
function getLatestVersionFromNpm(packageName) {
try {
const result = execCommand(`npm view ${packageName} version`);
return result.trim();
} catch (error) {
const errorMessage = error.message || error.toString();
if (errorMessage.includes('404')) {
log(`Package ${packageName} not found on npm registry (404) - treating as new package`);
return null;
}
log(`ERROR: Failed to check npm registry for ${packageName}: ${errorMessage}`);
throw new Error(`Failed to check npm registry for ${packageName}: ${errorMessage}`);
}
}
function getCurrentVersion(packagePath) {
const packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'));
return packageJson.version;
}
function bumpVersion(currentVersion, latestNpmVersion) {
const [major, minor, patch] = currentVersion.split('.').map(Number);
if (latestNpmVersion === null) {
return currentVersion;
}
const [npmMajor, npmMinor, npmPatch] = latestNpmVersion.split('.').map(Number);
if (currentVersion > latestNpmVersion) {
return `${major}.${minor}.${patch + 1}`;
}
if (latestNpmVersion > currentVersion) {
return `${npmMajor}.${npmMinor}.${npmPatch + 1}`;
}
return `${major}.${minor}.${patch + 1}`;
}
function updatePackageVersion(packagePath, packageName, newVersion) {
log(`Updating ${packageName} to version ${newVersion}`);
const packageJsonPath = path.join(packagePath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
return newVersion;
}
function publishPackage(packagePath, packageName) {
log(`Publishing ${packageName}`);
try {
execCommand('npm publish --provenance --access public', packagePath);
log(`Successfully published ${packageName}`);
} catch (error) {
log(`Failed to publish ${packageName}: ${error.message}`);
throw error;
}
}
function main() {
const isDryRun = process.argv.includes('--dry-run');
if (isDryRun) {
log('DRY RUN MODE - No packages will be published');
} else {
log('Starting fully automated package publishing');
}
log('Discovering packages...');
const packagePaths = discoverPackages();
log(`Found ${Object.keys(packagePaths).length} publishable packages: ${Object.keys(packagePaths).join(', ')}`);
log('Installing dependencies...');
execCommand('npm ci');
log('Building all packages...');
execCommand('npm run build');
const packagesToPublish = [];
for (const [packageName, packagePath] of Object.entries(packagePaths)) {
if (checkPackageChanges(packageName, packagePath)) {
log(`Changes detected for ${packageName}`);
packagesToPublish.push(packageName);
} else {
log(`No changes detected for ${packageName}`);
}
}
if (packagesToPublish.length === 0) {
log('No packages have changes, skipping publish');
process.exit(0);
}
log(`Found ${packagesToPublish.length} packages with changes: ${packagesToPublish.join(', ')}`);
for (const packageName of packagesToPublish) {
const packagePath = packagePaths[packageName];
const currentVersion = getCurrentVersion(packagePath);
const latestNpmVersion = getLatestVersionFromNpm(packageName);
if (latestNpmVersion === null) {
log(`Current version: ${currentVersion}, Latest on npm: (new package)`);
} else {
log(`Current version: ${currentVersion}, Latest on npm: ${latestNpmVersion}`);
}
const newVersion = bumpVersion(currentVersion, latestNpmVersion);
updatePackageVersion(packagePath, packageName, newVersion);
if (!isDryRun) {
publishPackage(packagePath, packageName);
} else {
log(`[DRY RUN] Would publish ${packageName}@${newVersion}`);
}
}
if (isDryRun) {
log('Dry run completed - no packages were actually published');
} else {
log('Automated publishing completed successfully');
}
}
if (require.main === module) {
main();
}

177
contrib/force-publish.js Normal file
View file

@ -0,0 +1,177 @@
#!/usr/bin/env node
/**
* Force publish all packages in monorepo
* - Builds everything
* - Publishes everything regardless of changes
* - Skips private packages
* - Uses npm trusted publishing (OIDC provenance) — no NPM_TOKEN needed
* - Gets latest version from npm and bumps appropriately
* - No git commits, just publishes
* - No manual interaction required
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
function discoverPackages() {
const packagesDir = 'packages';
const packagePaths = {};
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const packageDir of packageDirs) {
const packageJsonPath = path.join(packagesDir, packageDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.private) {
log(`Skipping private package: ${packageJson.name || packageDir}`);
continue;
}
if (packageJson.name) {
packagePaths[packageJson.name] = path.join(packagesDir, packageDir);
}
} catch (error) {
log(`Warning: Could not parse package.json in ${packageDir}: ${error.message}`);
}
}
}
return packagePaths;
}
function log(message) {
console.log(`[FORCE-PUBLISH] ${message}`);
}
function execCommand(command, cwd = process.cwd()) {
try {
return execSync(command, { cwd, stdio: 'pipe', encoding: 'utf8' });
} catch (error) {
throw new Error(`Command failed: ${command}\n${error.message}`);
}
}
function getLatestVersionFromNpm(packageName) {
try {
const result = execCommand(`npm view ${packageName} version`);
return result.trim();
} catch (error) {
const errorMessage = error.message || error.toString();
if (errorMessage.includes('404')) {
log(`Package ${packageName} not found on npm registry (404) - treating as new package`);
return null;
}
log(`ERROR: Failed to check npm registry for ${packageName}: ${errorMessage}`);
throw new Error(`Failed to check npm registry for ${packageName}: ${errorMessage}`);
}
}
function getCurrentVersion(packagePath) {
const packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'));
return packageJson.version;
}
function bumpVersion(currentVersion, latestNpmVersion) {
const [major, minor, patch] = currentVersion.split('.').map(Number);
if (latestNpmVersion === null) {
return currentVersion;
}
const [npmMajor, npmMinor, npmPatch] = latestNpmVersion.split('.').map(Number);
if (currentVersion > latestNpmVersion) {
return `${major}.${minor}.${patch + 1}`;
}
if (latestNpmVersion > currentVersion) {
return `${npmMajor}.${npmMinor}.${npmPatch + 1}`;
}
return `${major}.${minor}.${patch + 1}`;
}
function updatePackageVersion(packagePath, packageName, newVersion) {
log(`Updating ${packageName} to version ${newVersion}`);
const packageJsonPath = path.join(packagePath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
return newVersion;
}
function publishPackage(packagePath, packageName) {
log(`Publishing ${packageName}`);
try {
execCommand('npm publish --provenance --access public', packagePath);
log(`Successfully published ${packageName}`);
} catch (error) {
log(`Failed to publish ${packageName}: ${error.message}`);
throw error;
}
}
function main() {
const isDryRun = process.argv.includes('--dry-run');
if (isDryRun) {
log('DRY RUN MODE - No packages will be published');
} else {
log('Starting force publish - will publish ALL packages regardless of changes');
}
log('Discovering packages...');
const packagePaths = discoverPackages();
log(`Found ${Object.keys(packagePaths).length} publishable packages: ${Object.keys(packagePaths).join(', ')}`);
log('Installing dependencies...');
execCommand('npm ci');
log('Building all packages...');
execCommand('npm run build');
const allPackages = Object.keys(packagePaths);
log(`Force publishing ${allPackages.length} packages: ${allPackages.join(', ')}`);
for (const packageName of allPackages) {
const packagePath = packagePaths[packageName];
const currentVersion = getCurrentVersion(packagePath);
const latestNpmVersion = getLatestVersionFromNpm(packageName);
if (latestNpmVersion === null) {
log(`Current version: ${currentVersion}, Latest on npm: (new package)`);
} else {
log(`Current version: ${currentVersion}, Latest on npm: ${latestNpmVersion}`);
}
const newVersion = bumpVersion(currentVersion, latestNpmVersion);
updatePackageVersion(packagePath, packageName, newVersion);
if (!isDryRun) {
publishPackage(packagePath, packageName);
} else {
log(`[DRY RUN] Would publish ${packageName}@${newVersion}`);
}
}
if (isDryRun) {
log('Dry run completed - no packages were actually published');
} else {
log('Force publishing completed successfully');
}
}
if (require.main === module) {
main();
}

View file

@ -5,7 +5,7 @@
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"build": "npm run build --workspaces", "build": "npm run build -w packages/core -w packages/dapp -w packages/wallet -w packages/test-cli",
"test": "npm run test --workspaces --if-present", "test": "npm run test --workspaces --if-present",
"test:integration": "npm run test:integration --workspaces --if-present", "test:integration": "npm run test:integration --workspaces --if-present",
"dapp": "npm run dapp --workspace @wizardconnect/test-cli", "dapp": "npm run dapp --workspace @wizardconnect/test-cli",

View file

@ -1,6 +1,6 @@
{ {
"name": "@wizardconnect/core", "name": "@wizardconnect/core",
"version": "0.1.0", "version": "0.1.2",
"type": "module", "type": "module",
"description": "Transport and protocol primitives for WizardConnect", "description": "Transport and protocol primitives for WizardConnect",
"main": "dist/index.js", "main": "dist/index.js",
@ -11,11 +11,11 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"test": "vitest --config vitest.config.ts --run --passWithNoTests", "test": "vitest --config vitest.config.ts --run --passWithNoTests",
"lint:prettier": "prettier . --list-different", "lint:prettier": "prettier --ignore-path ../../.gitignore . --list-different",
"lint:eslint": "eslint .", "lint:eslint": "eslint .",
"lint": "npm run lint:eslint && npm run lint:prettier", "lint": "npm run lint:eslint && npm run lint:prettier",
"fix": "npm run fix:eslint && npm run fix:prettier", "fix": "npm run fix:eslint && npm run fix:prettier",
"fix:prettier": "prettier . --write", "fix:prettier": "prettier --ignore-path ../../.gitignore . --write",
"fix:eslint": "npm run lint:eslint -- --fix" "fix:eslint": "npm run lint:eslint -- --fix"
}, },
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@wizardconnect/dapp", "name": "@wizardconnect/dapp",
"version": "0.1.0", "version": "0.1.2",
"type": "module", "type": "module",
"description": "Dapp-side integration helpers for WizardConnect", "description": "Dapp-side integration helpers for WizardConnect",
"main": "dist/index.js", "main": "dist/index.js",
@ -11,11 +11,11 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"test": "vitest --config vitest.config.ts --run --passWithNoTests", "test": "vitest --config vitest.config.ts --run --passWithNoTests",
"lint:prettier": "prettier . --list-different", "lint:prettier": "prettier --ignore-path ../../.gitignore . --list-different",
"lint:eslint": "eslint .", "lint:eslint": "eslint .",
"lint": "npm run lint:eslint && npm run lint:prettier", "lint": "npm run lint:eslint && npm run lint:prettier",
"fix": "npm run fix:eslint && npm run fix:prettier", "fix": "npm run fix:eslint && npm run fix:prettier",
"fix:prettier": "prettier . --write", "fix:prettier": "prettier --ignore-path ../../.gitignore . --write",
"fix:eslint": "npm run lint:eslint -- --fix" "fix:eslint": "npm run lint:eslint -- --fix"
}, },
"dependencies": { "dependencies": {

View file

@ -12,11 +12,11 @@
"dev": "tsx src/cli.ts", "dev": "tsx src/cli.ts",
"dapp": "tsx src/cli.ts dapp", "dapp": "tsx src/cli.ts dapp",
"wallet": "tsx src/cli.ts wallet", "wallet": "tsx src/cli.ts wallet",
"lint:prettier": "prettier . --list-different", "lint:prettier": "prettier --ignore-path ../../.gitignore . --list-different",
"lint:eslint": "eslint .", "lint:eslint": "eslint .",
"lint": "npm run lint:eslint && npm run lint:prettier", "lint": "npm run lint:eslint && npm run lint:prettier",
"fix": "npm run fix:eslint && npm run fix:prettier", "fix": "npm run fix:eslint && npm run fix:prettier",
"fix:prettier": "prettier . --write", "fix:prettier": "prettier --ignore-path ../../.gitignore . --write",
"fix:eslint": "npm run lint:eslint -- --fix" "fix:eslint": "npm run lint:eslint -- --fix"
}, },
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@wizardconnect/wallet", "name": "@wizardconnect/wallet",
"version": "0.1.0", "version": "0.1.2",
"type": "module", "type": "module",
"description": "Wallet-side integration helpers for WizardConnect", "description": "Wallet-side integration helpers for WizardConnect",
"main": "dist/index.js", "main": "dist/index.js",
@ -12,11 +12,11 @@
"build": "tsc", "build": "tsc",
"test": "vitest --config vitest.config.ts --run --passWithNoTests", "test": "vitest --config vitest.config.ts --run --passWithNoTests",
"test:integration": "vitest --config vitest.integration.config.ts --run", "test:integration": "vitest --config vitest.integration.config.ts --run",
"lint:prettier": "prettier . --list-different", "lint:prettier": "prettier --ignore-path ../../.gitignore . --list-different",
"lint:eslint": "eslint .", "lint:eslint": "eslint .",
"lint": "npm run lint:eslint && npm run lint:prettier", "lint": "npm run lint:eslint && npm run lint:prettier",
"fix": "npm run fix:eslint && npm run fix:prettier", "fix": "npm run fix:eslint && npm run fix:prettier",
"fix:prettier": "prettier . --write", "fix:prettier": "prettier --ignore-path ../../.gitignore . --write",
"fix:eslint": "npm run lint:eslint -- --fix" "fix:eslint": "npm run lint:eslint -- --fix"
}, },
"dependencies": { "dependencies": {