diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..617d7fa --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/contrib/auto-publish.js b/contrib/auto-publish.js new file mode 100644 index 0000000..e12a7e0 --- /dev/null +++ b/contrib/auto-publish.js @@ -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(); +} diff --git a/contrib/force-publish.js b/contrib/force-publish.js new file mode 100644 index 0000000..0daab63 --- /dev/null +++ b/contrib/force-publish.js @@ -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(); +} diff --git a/package.json b/package.json index a128a7a..e6c884b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packages/*" ], "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:integration": "npm run test:integration --workspaces --if-present", "dapp": "npm run dapp --workspace @wizardconnect/test-cli", diff --git a/packages/core/package.json b/packages/core/package.json index 051b124..6216fc3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wizardconnect/core", - "version": "0.1.0", + "version": "0.1.2", "type": "module", "description": "Transport and protocol primitives for WizardConnect", "main": "dist/index.js", @@ -11,11 +11,11 @@ "scripts": { "build": "tsc", "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": "npm run lint:eslint && npm run lint: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" }, "dependencies": { diff --git a/packages/dapp/package.json b/packages/dapp/package.json index e7b10e7..671729c 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -1,6 +1,6 @@ { "name": "@wizardconnect/dapp", - "version": "0.1.0", + "version": "0.1.2", "type": "module", "description": "Dapp-side integration helpers for WizardConnect", "main": "dist/index.js", @@ -11,11 +11,11 @@ "scripts": { "build": "tsc", "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": "npm run lint:eslint && npm run lint: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" }, "dependencies": { diff --git a/packages/test-cli/package.json b/packages/test-cli/package.json index 778794d..a1407b2 100644 --- a/packages/test-cli/package.json +++ b/packages/test-cli/package.json @@ -12,11 +12,11 @@ "dev": "tsx src/cli.ts", "dapp": "tsx src/cli.ts dapp", "wallet": "tsx src/cli.ts wallet", - "lint:prettier": "prettier . --list-different", + "lint:prettier": "prettier --ignore-path ../../.gitignore . --list-different", "lint:eslint": "eslint .", "lint": "npm run lint:eslint && npm run lint: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" }, "dependencies": { diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 80b4399..a92f1f8 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -1,6 +1,6 @@ { "name": "@wizardconnect/wallet", - "version": "0.1.0", + "version": "0.1.2", "type": "module", "description": "Wallet-side integration helpers for WizardConnect", "main": "dist/index.js", @@ -12,11 +12,11 @@ "build": "tsc", "test": "vitest --config vitest.config.ts --run --passWithNoTests", "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": "npm run lint:eslint && npm run lint: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" }, "dependencies": {