diff --git a/.github/workflows/verify-packages.yml b/.github/workflows/verify-packages.yml new file mode 100644 index 000000000..f4ddae28a --- /dev/null +++ b/.github/workflows/verify-packages.yml @@ -0,0 +1,65 @@ +name: Verify Package Builds + +on: + push: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install tsx globally + run: npm install -g tsx + + - name: Get tags older than 6 months + id: get_tags + run: | + TAGS=$(git for-each-ref --sort=-taggerdate --format '%(refname:short) %(taggerdate:iso8601)' refs/tags | \ + awk -v date="$(date -d '6 months ago' +%Y-%m-%d)" '$2 < date {print $1}') + echo "tags<> $GITHUB_OUTPUT + echo "$TAGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Verify packages for each tag + id: verify_tags + run: | + set -e + mkdir -p results + echo "| Tag | Result | Differing Packages |" > results/summary.md + echo "|-----|--------|--------------------|" >> results/summary.md + echo "Tags to check:" + echo "${{ steps.get_tags.outputs.tags }}" + for TAG in ${{ steps.get_tags.outputs.tags }}; do + echo "\nProcessing tag: $TAG" + echo "Checking out $TAG" + git checkout $TAG + npm ci || { echo "| $TAG | npm ci failed | - |" >> results/summary.md; continue; } + echo "Running build-packages.ts for tag: $TAG" + DIFFERS_JSON=$(TAG=$TAG npx tsx ./build-packages.ts) + echo "build-packages.ts output for $TAG: $DIFFERS_JSON" + if [ "$DIFFERS_JSON" != "[]" ]; then + # Remove brackets and quotes for markdown table + DIFFERS_LIST=$(echo $DIFFERS_JSON | jq -r '. | join(", ")') + echo "| $TAG | ❌ Mismatch | $DIFFERS_LIST |" >> results/summary.md + else + echo "| $TAG | ✅ Match | - |" >> results/summary.md + fi + done + git checkout $GITHUB_SHA # Return to original commit + + - name: Print summary table + run: | + cat results/summary.md + +# continue: check github action status !!! diff --git a/build-packages.ts b/build-packages.ts new file mode 100644 index 000000000..14f3749c6 --- /dev/null +++ b/build-packages.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env tsx + +import * as fs from 'fs'; +import * as path from 'path'; +import { spawnSync } from 'child_process'; + +const ORIGINAL_DIR = process.cwd(); +const PACKAGES_DIR = path.resolve(ORIGINAL_DIR, 'packages'); + +// Get all package directories +const packages = fs.readdirSync(PACKAGES_DIR).filter(item => { + const itemPath = path.join(PACKAGES_DIR, item); + return fs.statSync(itemPath).isDirectory() && !item.startsWith('.'); +}); + +// console.debug(`Found ${packages.length} packages to process`); + +const NPM_VERSION_TAG = (process.env.NPM_VERSION_TAG || 'latest').replace(/^v/, ''); + +// Process all packages +const results = packages + .map(dir => { + const json = path.join(PACKAGES_DIR, dir, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(json, 'utf8')); + + if (pkg.private === true) { + // console.debug(`Skipping package: ${pkg.name}`); + return null; + } + + // console.debug(`\nProcessing package: ${pkg.name}`); + process.chdir(path.join(PACKAGES_DIR, dir)); + + // Run npm publish dry-run + const result = spawnSync('npm', ['publish', '--dry-run'], { encoding: 'utf8' }); + const output = `${result.stdout}\n${result.stderr}`; + + // Extract values using inline functions + const shasum = extractFromOutput(output, /npm notice shasum:\s+([a-f0-9]{40})/); + const integrity = extractFromOutput( + output, + /npm notice integrity:\s+(sha\d+-[A-Za-z0-9+/=]+(?:\[\.\.\.\][A-Za-z0-9+/=]*==?)?)/ + ); + + // console.debug(` Package: ${pkg.name}`); + // console.debug(` Tarball hash: ${integrity}`); + // console.debug(` Shasum: ${shasum}`); + + const registryUrl = `https://registry.npmjs.org/${pkg.name}/${NPM_VERSION_TAG}`; + const registryCheck = spawnSync('curl', [registryUrl], { + encoding: 'utf8', + }); + const registryData = JSON.parse(registryCheck.stdout); + const registryShasum = registryData.dist?.shasum; + + return { + name: pkg.name, + tarballHash: integrity, + shasum, + differs: registryShasum === shasum, + }; + }) + .filter(Boolean) as (PackageInfo & { differs: boolean })[]; + +// Restore original directory +process.chdir(ORIGINAL_DIR); + +// Output final results +// console.debug('\n=== PACKAGE BUILD RESULTS ==='); +// console.debug(results); + +// Output JSON array of differing packages +const differingPackages = results.filter(r => r.differs).map(r => r.name); +console.log(JSON.stringify(differingPackages)); + +// HELPERS +function extractFromOutput(output: string, regex: RegExp): string { + return ( + output + .split('\n') + .map(line => line.match(regex)?.[1]) + .find(Boolean) || 'Not found' + ); +} + +interface PackageInfo { + name: string; + tarballHash: string; + shasum: string; +}