diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8ee9e91..9a50ccd 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1 @@ -github: ["CoderDeltaLAN"] # si activas GitHub Sponsors en tu perfil, aparecerá -custom: - - "https://www.paypal.com/donate/?hosted_button_id=YVENCBNCZWVPW" +custom: ["https://www.paypal.com/donate/?hosted_button_id=YVENCBNCZWVPW"] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a9a7615 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + rebase-strategy: auto + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml new file mode 100644 index 0000000..3dd312b --- /dev/null +++ b/.github/workflows/auto-approve.yml @@ -0,0 +1,15 @@ +name: Auto-approve Dependabot +on: + pull_request: + types: [opened, ready_for_review, synchronize] +permissions: + pull-requests: write + contents: read +jobs: + approve: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: hmarr/auto-approve-action@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/auto-merge-deps.yml b/.github/workflows/auto-merge-deps.yml new file mode 100644 index 0000000..3f29112 --- /dev/null +++ b/.github/workflows/auto-merge-deps.yml @@ -0,0 +1,26 @@ +name: Auto-merge Dependabot when green +on: + pull_request: + types: [labeled, synchronize, reopened, ready_for_review] + check_suite: + types: [completed] +permissions: + contents: write + pull-requests: write +jobs: + enable-automerge: + runs-on: ubuntu-latest + steps: + - name: Find green Dependabot PRs + uses: peter-evans/find-pull-request@v3 + id: find + with: + author: dependabot[bot] + state: open + base: main + - name: Enable auto-merge (squash) + if: steps.find.outputs.pull-requests != '' + uses: peter-evans/enable-pull-request-automerge@v3 + with: + pull-request-number: ${{ fromJson(steps.find.outputs.pull-requests)[0].number }} + merge-method: squash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2175f87..447c148 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ concurrency: jobs: ping: - name: ping • ubuntu-latest + name: "ping • ubuntu-latest" runs-on: ubuntu-latest steps: - run: echo "ok" @@ -35,4 +35,78 @@ jobs: python-version: ${{ matrix.python }} - name: Smoke run: python -V -# ci:nudge 20250917T164347Z + + # ci:nudge 20250917T164347Z + ts: + name: "ts / node 22.x • ubuntu-latest" + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Detect Node workspace + id: guard + shell: bash + run: | + if find . -type f -name package.json -print -quit | grep -q .; then + echo "HAS_NODE=1" >> "$GITHUB_ENV" + else + echo "HAS_NODE=0" >> "$GITHUB_ENV" + fi + + - if: ${{ env.HAS_NODE == '1' }} + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: pnpm + + - if: ${{ env.HAS_NODE == '1' }} + name: Ensure pnpm + shell: bash + run: | + if ! command -v pnpm >/dev/null 2>&1; then + corepack enable || true + corepack prepare pnpm@latest --activate || true + fi + pnpm -v || echo "pnpm not found (best-effort)" + + - if: ${{ env.HAS_NODE == '1' }} + name: Detect package manager + id: pm + shell: bash + run: | + if [ -f pnpm-lock.yaml ]; then echo "PM=pnpm" >> "$GITHUB_ENV"; + elif [ -f package-lock.json ]; then echo "PM=npm" >> "$GITHUB_ENV"; + elif [ -f yarn.lock ]; then echo "PM=yarn" >> "$GITHUB_ENV"; + else echo "PM=none" >> "$GITHUB_ENV"; fi + + - if: ${{ env.HAS_NODE == '1' }} + name: Install deps (best-effort) + shell: bash + run: | + case "$PM" in + pnpm) pnpm i --frozen-lockfile || true ;; + npm) npm ci || true ;; + yarn) corepack yarn install --immutable || true ;; + *) echo "no lockfile" ;; + esac + + - if: ${{ env.HAS_NODE == '1' }} + name: Test smoke (best-effort) + shell: bash + run: | + if [ -f package.json ] && node -e "p=require('./package.json');process.exit(p.scripts&&p.scripts.test?0:1)"; then + case "$PM" in + pnpm) pnpm -s test || true ;; + npm) npm -s test || true ;; + yarn) corepack yarn -s test || true ;; + *) echo "no PM; skip test" ;; + esac + else + echo "no tests" + fi + + - if: ${{ env.HAS_NODE != '1' }} + name: Skip (no package.json) + run: | + echo "skip ts: no Node workspace" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..1ce3f7d --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,30 @@ +name: dependabot-auto-merge + +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + if: ${{ github.actor == 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - name: Fetch metadata + id: meta + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable PR auto-merge (patch/minor only) + if: | + steps.meta.outputs.update-type == 'version-update:semver-patch' || + steps.meta.outputs.update-type == 'version-update:semver-minor' + uses: peter-evans/enable-pull-request-automerge@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pull-request-number: ${{ github.event.pull_request.number }} + merge-method: squash diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..4e1cde8 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,49 @@ +name: publish-pypi + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + build-publish: + name: Build & publish to PyPI + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install build backend + run: python -m pip install --upgrade pip build + + - name: Build sdist & wheel + run: python -m build --sdist --wheel --outdir dist/ + + - name: Check tag matches version (only on tags) + if: startsWith(github.ref, 'refs/tags/') + shell: bash + run: | + PYPROJECT_VERSION=$(python - <<'PY' + import tomllib + print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"]) + PY + ) + TAG="${GITHUB_REF_NAME#v}" + echo "pyproject.toml: $PYPROJECT_VERSION / tag: $TAG" + test "$TAG" = "$PYPROJECT_VERSION" + + - name: Publish to PyPI (Trusted Publisher) + if: startsWith(github.ref, 'refs/tags/') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + print_hash: true diff --git a/.github/workflows/ts-ci.yml b/.github/workflows/ts-ci.yml index 64bb2bb..41c3850 100644 --- a/.github/workflows/ts-ci.yml +++ b/.github/workflows/ts-ci.yml @@ -1,75 +1,106 @@ -name: "TypeScript/Node CI (reusable)" +name: TS CI on: - workflow_call: - inputs: - node_versions: - type: string - required: false - default: '["22.x"]' - os: - type: string - required: false - default: '["ubuntu-latest"]' - package_manager: - type: string - required: false - default: pnpm - test_command: - type: string - required: false - default: 'pnpm -s test || echo "no tests"' + push: + paths: + - "**/*.ts" + - "**/*.tsx" + - "package.json" + - "pnpm-lock.yaml" + - "eslint.*" + - ".eslintrc.*" + - "tsconfig*.json" + pull_request: + paths: + - "**/*.ts" + - "**/*.tsx" + - "package.json" + - "pnpm-lock.yaml" + - "eslint.*" + - ".eslintrc.*" + - "tsconfig*.json" + workflow_dispatch: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: - build: - name: node${{ matrix.node }} • ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - node: ${{ fromJson(inputs.node_versions) }} - os: ${{ fromJson(inputs.os) }} - + ts: + name: "ts / node 22.x • ubuntu-latest" + runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v4 - - name: Guard — skip if no Node project + - name: Detect Node workspace id: guard shell: bash run: | - if find -L . -type f -name package.json \ - -not -path "./.git/*" -not -path "*/node_modules/*" \ - -print -quit | grep -q .; then - echo "has_pkg=true" >> "$GITHUB_OUTPUT" + if find . -type f -name package.json -print -quit | grep -q .; then + echo "HAS_NODE=1" >> "$GITHUB_ENV" else - echo "has_pkg=false" >> "$GITHUB_OUTPUT" + echo "HAS_NODE=0" >> "$GITHUB_ENV" fi - - uses: actions/setup-node@v4 - if: ${{ steps.guard.outputs.has_pkg == 'true' }} + - if: ${{ env.HAS_NODE == '1' }} + uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node }} - cache: ${{ inputs.package_manager }} + node-version: 22.x + cache: pnpm - - name: Install - if: ${{ steps.guard.outputs.has_pkg == 'true' }} + - if: ${{ env.HAS_NODE == '1' }} + name: Ensure pnpm shell: bash run: | corepack enable || true - case "${{ inputs.package_manager }}" in - pnpm) pnpm i --frozen-lockfile || pnpm i ;; - npm) npm ci || npm i ;; - yarn) yarn install --frozen-lockfile || yarn install ;; - *) echo "unknown package manager: ${{ inputs.package_manager }}" ;; - esac + corepack prepare pnpm@latest --activate || true + if ! command -v pnpm >/dev/null; then + npm i -g pnpm@9 || true + fi + pnpm -v || true + + - if: ${{ env.HAS_NODE == '1' }} + name: Install deps (pnpm) + shell: bash + run: | + if [ -f pnpm-lock.yaml ]; then + pnpm i --frozen-lockfile || true + else + echo "no pnpm lock; skip install" + fi + + - if: ${{ env.HAS_NODE == '1' }} + name: Type check (tsc --noEmit if tsconfig present) + shell: bash + run: | + if [ -f tsconfig.json ] || ls -1 tsconfig.*.json >/dev/null 2>&1; then + npx -y typescript@latest --version + npx -y tsc --noEmit + else + echo "no tsconfig; skip tsc" + fi + + - if: ${{ env.HAS_NODE == '1' }} + name: Lint (eslint if config present) + shell: bash + run: | + if ls -1 .eslintrc.* eslint.config.* >/dev/null 2>&1; then + npx -y eslint . --max-warnings=0 + else + echo "no eslint config; skip eslint" + fi - - name: Test - if: ${{ steps.guard.outputs.has_pkg == 'true' && inputs.test_command != '' }} - run: ${{ inputs.test_command }} + - if: ${{ env.HAS_NODE == '1' }} + name: Tests (pnpm test if exists) + shell: bash + run: | + if [ -f package.json ] && node -e "p=require('./package.json');process.exit(p.scripts&&p.scripts.test?0:1)"; then + pnpm -s test || true + else + echo "no tests" + fi + + - if: ${{ env.HAS_NODE != '1' }} + name: Skip (no Node workspace) + run: | + echo "skip ts-ci: no Node workspace" diff --git a/.gitignore b/.gitignore index c26d5af..aa1a9b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ node_modules/ .mypy_cache/ .pytest_cache/ sbom-*.json +.github/workflows/*.bak.* +pyproject.toml.bak diff --git a/README.md b/README.md index 90ea168..f66732f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ +

+ ci-matrix-starter +

+ +

+ + CI + + + CodeQL + + + PyPI + + + Downloads + + + License + + + Sponsor + +

+ +

Reusable GitHub Actions CI for Python & TypeScript

+ +**ci-matrix-starter** es una plantilla CI reusable que entrega matrices Py/TS listas para producción con: +**always-green CI**, **SBOM**, **CodeQL**, **pre-commit**, **Dependabot auto-merge** y **Publicación a PyPI (Trusted Publisher OIDC)**. +Ideal para equipos que necesitan **DevSecOps** inmediato y verificable. + +> _Keywords_: github actions, reusable workflows, python, typescript, pnpm, node, sbom, sigstore, cosign, codeql, pre-commit, dependabot, pypi, devsecops, supply-chain, always-green ci, matrix. + # ⭐ ci-matrix-starter — Reusable CI Workflows (Python & TypeScript) A lean, production-ready **GitHub Actions starter** that ships **reusable CI workflows** for **Python (3.11/3.12)** and **TypeScript/Node 20**. diff --git a/pyproject.toml b/pyproject.toml index 1c1726d..1c7361f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ci-matrix-starter" -version = "0.1.0" +version = "0.1.4" description = "Reusable CI workflows for Py/TS with SBOM & signatures." authors = ["CoderDeltaLAN "] readme = "README.md"