diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000..455d1bd --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,54 @@ +name: GitHub Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: 🚀 Create GitHub Release + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + fetch-depth: 0 + + - name: 🏷️ Create GitHub Release + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + with: + script: | + const tag = context.ref.replace('refs/tags/', ''); + const version = tag.replace(/^v/, ''); + + const installBlock = [ + '## Install', + '```sh', + `npm install @mcabreradev/filter@${version}`, + `pnpm add @mcabreradev/filter@${version}`, + `yarn add @mcabreradev/filter@${version}`, + '```', + '', + `📦 [View on npm](https://www.npmjs.com/package/@mcabreradev/filter/v/${version})`, + '', + '---', + '', + ].join('\n'); + + const { data: release } = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + name: `Release ${tag}`, + body: installBlock, + generate_release_notes: true, + draft: false, + prerelease: false, + }); + + console.log(`✅ Release created: ${release.html_url}`); diff --git a/.github/workflows/npm-publish-manual.yml b/.github/workflows/npm-publish-manual.yml index a026c6b..9f5be63 100644 --- a/.github/workflows/npm-publish-manual.yml +++ b/.github/workflows/npm-publish-manual.yml @@ -51,11 +51,17 @@ jobs: echo "Build files:" ls -la build/ + - name: 🔒 Security audit + run: pnpm audit --audit-level=high + - name: 🔍 Running Check/Test Suite run: pnpm run check - name: 🔍 Determine version bump from PR title or labels id: version + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }} run: | VERSION_TYPE="" DETECTION_METHOD="" @@ -63,7 +69,6 @@ jobs: # ============================================ # PASO 1: Intentar detectar por título del PR # ============================================ - PR_TITLE="${{ github.event.pull_request.title }}" echo "📋 PR Title: $PR_TITLE" # Extraer el tipo (soporta: "type: msg", "type(scope): msg", "type!: msg") @@ -100,8 +105,7 @@ jobs: if [ -z "$VERSION_TYPE" ]; then echo "⚠️ No valid conventional commit format in title, checking labels..." - LABELS='${{ toJSON(github.event.pull_request.labels) }}' - LABEL_NAMES=$(echo "$LABELS" | jq -r '.[].name' 2>/dev/null || echo "") + LABEL_NAMES=$(echo "$PR_LABELS" | jq -r '.[].name' 2>/dev/null || echo "") if [ -n "$LABEL_NAMES" ]; then echo "🏷️ Labels found: $LABEL_NAMES" @@ -144,26 +148,36 @@ jobs: echo "type=$VERSION_TYPE" >> $GITHUB_OUTPUT - - name: 📈 Bump version + - name: 📈 Bump version (local only) run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" pnpm version ${{ steps.version.outputs.type }} --no-git-tag-version + + - name: 🔎 Check version doesn't already exist on npm + run: | NEW_VERSION=$(node -p "require('./package.json').version") echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + if npm view "@mcabreradev/filter@$NEW_VERSION" version 2>/dev/null | grep -q "$NEW_VERSION"; then + echo "❌ Version $NEW_VERSION already exists on npm. Aborting to avoid duplicate publish." + exit 1 + fi + echo "✅ Version $NEW_VERSION is available for publishing." + + - name: 🚀 Publish to npm + run: pnpm publish --no-git-checks --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: 📝 Commit version bump + run: | git add package.json git commit -m "chore: bump version to $NEW_VERSION [skip ci]" - name: 🔄 Push version bump - run: | - git push origin main + run: git push origin main - - name: 📝 Create Git Tag + - name: 🏷️ Create Git Tag run: | - git tag -a "v${{ env.NEW_VERSION }}" -m "Release v${{ env.NEW_VERSION }}" - git push origin "v${{ env.NEW_VERSION }}" - - - name: 🚀 Publish to npm - run: pnpm publish --no-git-checks --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" + git push origin "v$NEW_VERSION" diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml new file mode 100644 index 0000000..83b6dbf --- /dev/null +++ b/.github/workflows/pr-title-lint.yml @@ -0,0 +1,54 @@ +name: PR Title Lint + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + branches: + - main + +permissions: + pull-requests: read + +jobs: + lint-pr-title: + name: 📋 Validate PR Title + runs-on: ubuntu-latest + + steps: + - name: ✅ Validate conventional commit format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|major|breaking|minor|feature|bugfix|hotfix|patch)(\([^)]+\))?!?:[[:space:]].+' + + echo "📋 PR Title: $PR_TITLE" + echo "" + + if echo "$PR_TITLE" | grep -qE "$PATTERN"; then + echo "✅ PR title follows conventional commits format." + else + echo "❌ PR title does not follow conventional commits format." + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Expected format:" + echo "" + echo " (): " + echo " !: (breaking change)" + echo " ()!: (breaking change)" + echo "" + echo " Valid types:" + echo " MINOR bump → feat, feature, minor" + echo " PATCH bump → fix, bugfix, hotfix, chore, docs," + echo " style, refactor, perf, test, build," + echo " ci, revert" + echo " MAJOR bump → major, breaking, or any type with !" + echo "" + echo " Examples:" + echo " feat: add \$in operator support" + echo " fix(core): resolve cache invalidation bug" + echo " feat!: remove deprecated find() API" + echo " chore(deps): update dependencies" + echo " refactor(predicate): simplify factory pattern" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + exit 1 + fi diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..218a1c5 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,101 @@ +name: Security Audit + +on: + schedule: + - cron: '0 8 * * 1' # every Monday at 08:00 UTC + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + audit: + name: 🔒 Security Audit + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + + - name: 📦 Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + with: + version: 10 + + - name: ⎔ Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: 20 + cache: 'pnpm' + + - name: 📥 Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: 🔍 Audit (high & critical) + id: audit_high + run: pnpm audit --audit-level=high + continue-on-error: true + + - name: 📋 Audit report (all severities) + id: audit_full + run: | + echo "## Security Audit Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + pnpm audit --json 2>/dev/null | node -e " + const chunks = []; + process.stdin.on('data', c => chunks.push(c)); + process.stdin.on('end', () => { + try { + const data = JSON.parse(chunks.join('')); + const meta = data.metadata || {}; + const vulns = meta.vulnerabilities || {}; + const total = (vulns.critical||0) + (vulns.high||0) + (vulns.moderate||0) + (vulns.low||0) + (vulns.info||0); + console.log('| Severity | Count |'); + console.log('|---|---|'); + if (vulns.critical) console.log('| 🔴 Critical | ' + vulns.critical + ' |'); + if (vulns.high) console.log('| 🔴 High | ' + vulns.high + ' |'); + if (vulns.moderate) console.log('| 🟡 Moderate | ' + vulns.moderate + ' |'); + if (vulns.low) console.log('| 🟢 Low | ' + vulns.low + ' |'); + if (total === 0) console.log('| ✅ None | 0 |'); + } catch(e) { + console.log('Could not parse audit output.'); + } + }); + " >> $GITHUB_STEP_SUMMARY + continue-on-error: true + + - name: 🚨 Open issue if high/critical vulnerabilities found (scheduled only) + if: steps.audit_high.outcome == 'failure' && github.event_name == 'schedule' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + with: + script: | + const title = '🔒 Security vulnerabilities found in dependencies'; + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'security', + }); + const existing = issues.find(i => i.title === title); + if (!existing) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body: `The weekly security audit found high or critical vulnerabilities in dependencies.\n\nRun \`pnpm audit\` locally to see details and \`pnpm audit --fix\` or update \`pnpm.overrides\` to resolve them.\n\nWorkflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`, + labels: ['security'], + }); + } + + - name: ❌ Fail if high/critical vulnerabilities found + if: steps.audit_high.outcome == 'failure' + run: | + echo "High or critical vulnerabilities were found. See audit report above." + exit 1 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 07b4230..8b7c872 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -3,12 +3,14 @@ name: Code Check on: push: branches: + - main - dev - stage - master - prod pull_request: branches: + - main - dev - stage - master @@ -60,6 +62,9 @@ jobs: - name: 📊 Generate coverage report run: pnpm run test:coverage + - name: 🏗️ Build + run: pnpm run build + - name: Re Check All run: pnpm run check diff --git a/.gitignore b/.gitignore index f747862..9ded70e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ typings/ # next.js build output .next -.npmrc # Type tests __test__/types/*.js @@ -71,4 +70,4 @@ build/index.test-d.ts # VitePress docs/.vitepress/dist -docs/.vitepress/cache \ No newline at end of file +docs/.vitepress/cache diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ab170b1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} +@mcabreradev:registry=https://registry.npmjs.org diff --git a/package.json b/package.json index c2cc2ce..ae9bfcf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mcabreradev/filter", - "version": "5.9.0", + "version": "5.8.3", "type": "module", "description": "A powerful, SQL-like array filtering library for TypeScript and JavaScript with advanced pattern matching, MongoDB-style operators, deep object comparison, and zero dependencies", "scripts": { @@ -297,8 +297,7 @@ ], "publishConfig": { "access": "public", - "registry": "https://registry.npmjs.org", - "provenance": true + "registry": "https://registry.npmjs.org" }, "pnpm": { "overrides": {