Skip to content

fix: add missing website-generator script to textchecker-element #7

fix: add missing website-generator script to textchecker-element

fix: add missing website-generator script to textchecker-element #7

name: Check NPM Provenance
on:
pull_request:
paths:
- '**/package.json'
- '.github/workflows/check-provenance.yml'
push:
branches:
- master
paths:
- '**/package.json'
workflow_dispatch:
permissions: {}
jobs:
check-provenance:
name: Check Package Provenance
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
has_missing: ${{ steps.check.outputs.has_missing }}
has_unpublished: ${{ steps.check.outputs.has_unpublished }}
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
- name: Check npm provenance
id: check
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const fs = require('node:fs');
const path = require('node:path');
const { execSync } = require('node:child_process');
// Get workspace packages using pnpm
function getWorkspacePackages() {
try {
// Use pnpm to list all workspace packages
const output = execSync('pnpm list -r --json --depth -1', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
const packages = JSON.parse(output);
// Filter out private packages and return package info
return packages
.filter(pkg => !pkg.private)
.map(pkg => ({
name: pkg.name,
version: pkg.version,
private: pkg.private || false
}));
} catch (error) {
console.error('Error getting workspace packages:', error);
// Fallback to reading lerna.json or root package.json
const rootPkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const workspaces = rootPkg.workspaces || [];
const packages = [];
for (const pattern of workspaces) {
const basePath = pattern.replace('/*', '');
if (!fs.existsSync(basePath)) continue;
const dirs = fs.readdirSync(basePath);
for (const dir of dirs) {
const packagePath = path.join(basePath, dir, 'package.json');
if (fs.existsSync(packagePath)) {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
if (!packageJson.private) {
packages.push(packageJson);
}
}
}
}
return packages;
}
}
// Check package status (published + provenance) in a single fetch
async function checkPackageStatus(packageName) {
try {
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`);
if (!response.ok) {
return { published: false, hasProvenance: false };
}
const data = await response.json();
// Get latest version
const latestVersion = data['dist-tags']?.latest;
if (!latestVersion) {
return { published: true, hasProvenance: false };
}
// Check if the latest version has attestations
const versionData = data.versions?.[latestVersion];
const hasProvenance = !!(versionData?.dist?.attestations);
return { published: true, hasProvenance };
} catch (error) {
console.error(`Error checking status for ${packageName}:`, error);
return { published: false, hasProvenance: false };
}
}
console.log('🔍 Checking npm provenance for public packages...\n');
// Get all public packages from workspace
const publicPackages = getWorkspacePackages();
const results = {
withProvenance: [],
withoutProvenance: [],
notPublished: []
};
for (const pkg of publicPackages) {
const status = await checkPackageStatus(pkg.name);
if (!status.published) {
results.notPublished.push(pkg.name);
console.log(`⏭️ ${pkg.name}: Not published yet`);
} else if (status.hasProvenance) {
results.withProvenance.push(pkg.name);
console.log(`✅ ${pkg.name}: Has provenance`);
} else {
results.withoutProvenance.push(pkg.name);
console.log(`❌ ${pkg.name}: Missing provenance`);
}
}
// Summary
console.log('\n📊 Summary:');
console.log(` Total public packages: ${publicPackages.length}`);
console.log(` With provenance: ${results.withProvenance.length}`);
console.log(` Without provenance: ${results.withoutProvenance.length}`);
console.log(` Not published: ${results.notPublished.length}`);
// Save results for next steps
fs.writeFileSync('provenance-results.json', JSON.stringify(results, null, 2));
// Set outputs
core.setOutput('has_missing', results.withoutProvenance.length > 0);
core.setOutput('has_unpublished', results.notPublished.length > 0);
core.setOutput('missing_packages', results.withoutProvenance);
core.setOutput('unpublished_packages', results.notPublished);
- name: Upload results
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: provenance-results
path: provenance-results.json
retention-days: 1
post-comment:
name: Post PR Comment
runs-on: ubuntu-latest
needs: check-provenance
if: (needs.check-provenance.outputs.has_missing == 'true' || needs.check-provenance.outputs.has_unpublished == 'true') && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download results
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: provenance-results
- name: Comment on PR
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('node:fs');
const results = JSON.parse(fs.readFileSync('provenance-results.json', 'utf8'));
let comment = '## 📦 NPM Package Status\n\n';
if (results.notPublished.length > 0) {
comment += '### New Packages (Not Published Yet)\n\n';
comment += 'Run the following commands to set up OIDC and publish:\n\n';
results.notPublished.forEach(pkg => {
comment += `- [ ] \`npx setup-npm-trusted-publish ${pkg}\`\n`;
});
comment += '\n';
}
if (results.withoutProvenance.length > 0) {
comment += '### Published Packages Missing OIDC Configuration\n\n';
comment += 'Configure OIDC for these packages:\n\n';
results.withoutProvenance.forEach(pkg => {
comment += `- [ ] [${pkg}](https://www.npmjs.com/package/${pkg}/access)\n`;
});
comment += '\n**Setup Instructions:**\n';
comment += '1. Click each package link above\n';
comment += '2. Click "Add trusted publisher"\n';
comment += '3. Configure with:\n';
comment += ` - Repository: \`${context.repo.owner}/${context.repo.repo}\`\n`;
comment += ' - Workflow: `.github/workflows/release.yml`\n';
comment += ' - Environment: npm\n';
}
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('NPM Package Status')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}