chore: add release test workflow for dry-run testing #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | ||
| on: | ||
| # Trigger on version tags (e.g., v1.0.0, v1.2.3) | ||
| push: | ||
| tags: | ||
| - 'v*.*.*' | ||
| # Security: Minimal permissions following principle of least privilege | ||
| permissions: | ||
| contents: write # Create releases | ||
| id-token: write # For npm publish (OIDC) | ||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # Security: Using version tags (consider pinning to commit SHAs for production) | ||
| # To get latest SHA: https://api.github.com/repos/actions/checkout/commits/main | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # Need full history for changelog | ||
| # Note: github.ref automatically points to the tag that triggered this workflow | ||
| # Security: Using version tags (consider pinning to commit SHAs for production) | ||
| - name: Use Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20.x' | ||
| registry-url: 'https://registry.npmjs.org' | ||
| # Security: No cache needed - this repo has zero dependencies | ||
| # Security: Verify no package installations in workflow files | ||
| - name: Security Check - No Package Installations | ||
| run: | | ||
| echo "🔍 Scanning workflow files for security..." | ||
| if grep -rE "(npm install|npm ci|yarn install|pnpm install|bun install|pip install|pip3 install)" .github/workflows/ 2>/dev/null; then | ||
| echo "❌ ERROR: Package installation detected in workflow files!" | ||
| echo "This repository uses zero dependencies - no installs should be needed" | ||
| exit 1 | ||
| fi | ||
| echo "✅ No package installations found in workflows" | ||
| # Security: Verify package.json has zero dependencies | ||
| - name: Security Check - Zero Dependencies | ||
| run: | | ||
| echo "🔍 Verifying package.json has zero dependencies..." | ||
| if node -e "const pkg = require('./package.json'); if (pkg.dependencies || pkg.devDependencies) { console.error('ERROR: Dependencies found!'); console.error('dependencies:', pkg.dependencies); console.error('devDependencies:', pkg.devDependencies); process.exit(1); } else { console.log('✅ Zero dependencies confirmed'); }"; then | ||
| echo "✅ Package has zero dependencies - safe from supply chain attacks" | ||
| else | ||
| echo "❌ ERROR: Dependencies detected in package.json" | ||
| echo "This is a security risk - this package should have zero dependencies" | ||
| exit 1 | ||
| fi | ||
| # Security: Verify no node_modules directory exists | ||
| - name: Security Check - No node_modules | ||
| run: | | ||
| if [ -d "node_modules" ]; then | ||
| echo "❌ ERROR: node_modules directory found!" | ||
| echo "This repository should not have any installed dependencies" | ||
| exit 1 | ||
| fi | ||
| echo "✅ No node_modules directory found" | ||
| - name: Extract version from tag | ||
| id: version | ||
| run: | | ||
| # Extract version from tag (remove 'v' prefix) | ||
| VERSION="${GITHUB_REF#refs/tags/v}" | ||
| echo "version=$VERSION" >> $GITHUB_OUTPUT | ||
| echo "tag=v$VERSION" >> $GITHUB_OUTPUT | ||
| echo "📦 Version: $VERSION" | ||
| - name: Verify version matches package.json | ||
| run: | | ||
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | ||
| if [ "$PACKAGE_VERSION" != "${{ steps.version.outputs.version }}" ]; then | ||
| echo "❌ ERROR: Version mismatch!" | ||
| echo " Tag version: ${{ steps.version.outputs.version }}" | ||
| echo " package.json version: $PACKAGE_VERSION" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Version matches package.json" | ||
| # Security: Verify package.json integrity | ||
| - name: Security Check - Package.json Integrity | ||
| run: | | ||
| echo "🔍 Verifying package.json integrity..." | ||
| # Check for suspicious install scripts that might indicate tampering | ||
| node -e " | ||
| const pkg = require('./package.json'); | ||
| const scripts = pkg.scripts || {}; | ||
| const suspicious = Object.keys(scripts).filter(k => | ||
| k.includes('install') || k.includes('post') || k.includes('pre') | ||
| ); | ||
| if (suspicious.length > 0) { | ||
| console.error('❌ ERROR: Suspicious scripts found:', suspicious); | ||
| console.error('Scripts:', suspicious.map(s => scripts[s]).join(', ')); | ||
| process.exit(1); | ||
| } | ||
| console.log('✅ Package.json integrity check passed'); | ||
| " | ||
| - name: Run tests | ||
| run: | | ||
| echo "🧪 Running tests before release..." | ||
| npm run test:smoke | ||
| # Security: Verify npm token is set (but don't expose it) | ||
| # Note: In GitHub Actions, secrets are always non-empty strings (masked if not set) | ||
| # We'll verify it works by checking if npm can authenticate | ||
| - name: Security Check - NPM Token | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| echo "🔍 Verifying NPM_TOKEN..." | ||
| # Try to get npm user info (this will fail if token is invalid/missing) | ||
| if npm whoami --registry=https://registry.npmjs.org 2>/dev/null; then | ||
| echo "✅ NPM_TOKEN is valid and configured" | ||
| else | ||
| echo "❌ ERROR: NPM_TOKEN is missing or invalid!" | ||
| echo "Please add a valid NPM_TOKEN to repository secrets" | ||
| exit 1 | ||
| fi | ||
| # Security: Dry run first to verify package contents | ||
| - name: Security Check - npm publish dry run | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| echo "🔍 Running npm publish dry run to verify package contents..." | ||
| npm publish --dry-run --access public | ||
| echo "✅ Dry run successful - package contents verified" | ||
| - name: Publish to npm | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| echo "📦 Publishing to npm..." | ||
| # Security: Use --dry-run flag removed, but we already verified above | ||
| npm publish --access public | ||
| echo "✅ Published @dreamhorizonorg/sentinel@${{ steps.version.outputs.version }} to npm" | ||
| - name: Generate release notes from CHANGELOG | ||
| id: release_notes | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| TAG="${{ steps.version.outputs.tag }}" | ||
| REPO="${{ github.repository }}" | ||
| # Extract changelog entry for this version | ||
| if [ -f CHANGELOG.md ]; then | ||
| # Get changelog section for this version (from ## [VERSION] to next ##) | ||
| # Use a more robust awk script that handles multiline content | ||
| CHANGELOG_SECTION=$(awk -v version="$VERSION" ' | ||
| /^## \[/ { | ||
| if (found) exit | ||
| if ($0 ~ "## \\[" version "\\]") { | ||
| found = 1 | ||
| next | ||
| } | ||
| } | ||
| found { print } | ||
| ' CHANGELOG.md) | ||
| if [ -z "$CHANGELOG_SECTION" ]; then | ||
| # Fallback if version not found in changelog | ||
| CHANGELOG_SECTION="## Changes | ||
| See [CHANGELOG.md](https://github.com/$REPO/blob/main/CHANGELOG.md) for details." | ||
| fi | ||
| else | ||
| CHANGELOG_SECTION="## Changes | ||
| See commit history for details." | ||
| fi | ||
| # Create release notes using a here-doc with proper escaping | ||
| { | ||
| echo "# 🛡️ Sentinel Package Manager $VERSION" | ||
| echo "" | ||
| echo "$CHANGELOG_SECTION" | ||
| echo "" | ||
| echo "## 📦 Installation" | ||
| echo "" | ||
| echo "\`\`\`bash" | ||
| echo "npm install -g @dreamhorizonorg/sentinel@$VERSION" | ||
| echo "\`\`\`" | ||
| echo "" | ||
| echo "## 🔗 Links" | ||
| echo "" | ||
| echo "- **npm**: https://www.npmjs.com/package/@dreamhorizonorg/sentinel/v/$VERSION" | ||
| echo "- **GitHub**: https://github.com/$REPO/releases/tag/$TAG" | ||
| echo "- **Documentation**: https://github.com/$REPO#readme" | ||
| } > release_notes.md | ||
| # Security: Using version tag (consider pinning to commit SHA) | ||
| # Latest SHA: https://api.github.com/repos/softprops/action-gh-release/commits/main | ||
| - name: Create GitHub Release | ||
| uses: softprops/action-gh-release@v1 | ||
| with: | ||
| tag_name: ${{ steps.version.outputs.tag }} | ||
| name: Release ${{ steps.version.outputs.tag }} | ||
| body_path: release_notes.md | ||
| draft: false | ||
| prerelease: ${{ contains(steps.version.outputs.version, '-') || contains(steps.version.outputs.version, 'alpha') || contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'rc') }} | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Release summary | ||
| run: | | ||
| echo "## 🎉 Release Complete!" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "**Version**: ${{ steps.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY | ||
| echo "**npm**: https://www.npmjs.com/package/@dreamhorizonorg/sentinel/v/${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY | ||
| echo "**GitHub Release**: https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY | ||