diff --git a/.cursor/rules/processmaker-org-github.mdc b/.cursor/rules/processmaker-org-github.mdc new file mode 100644 index 0000000..82edbff --- /dev/null +++ b/.cursor/rules/processmaker-org-github.mdc @@ -0,0 +1,32 @@ +--- +description: ProcessMaker/.github org-wide reusable workflows — repo purpose and CI patterns +alwaysApply: true +--- + +# ProcessMaker `.github` (org-wide) + +This repository is [ProcessMaker/.github](https://github.com/ProcessMaker/.github): **shared GitHub Actions workflows, scripts, and composite actions** for the ProcessMaker GitHub org. Consumer repositories do not copy this logic; they **call** a reusable workflow via `uses: //.github/workflows/.yml@` and `secrets: inherit` (pin is often `develop` or `main`). The org may still point `uses:` at the core app repo or at this repo depending on workflow—**follow the actual `uses:` in caller repos** when tracing behavior. + +## Repo “shape” the AI must keep in mind + +- **ProcessMaker core** (e.g. main application repo) and **other repos** (enterprise add-ons, `package-*`, customer/custom packages) **all wire the same reusable workflow entrypoint**. Locally you are often editing the **shared** workflow that runs in **many different repository contexts**. +- When changing jobs or steps, assume the caller may be **core**, **enterprise**, or **custom/package** unless you have verified otherwise. + +## Critical pattern: gate behavior with `if:` + +**Every consumer repo is configured to run the shared workflow** (same pattern, different workflow `name`, sometimes different `@ref` — commonly `develop` or `main`; the org tries to keep those aligned to avoid mass updates). + +Therefore: + +- Anything that should **only** run for certain repos (core vs package vs enterprise-only, etc.) **must** be guarded with job-level or step-level **`if:`** (or equivalent conditions), not by hoping only the “right” repos call the workflow. +- Prefer explicit conditions on **`github.repository`**, **`github.event.pull_request.head.repo.name`**, **`github.event.repository.name`**, or env vars your workflow already sets (e.g. `CI_PROJECT` / naming conventions) rather than implicit assumptions. + +## Consumer example + +Package repos typically have a thin wrapper workflow whose only real differences are **display name** and **pin branch**; see e.g. [package-webentry `deploy-package.yml`](https://github.com/ProcessMaker/package-webentry/blob/develop/.github/workflows/deploy-package.yml) (`uses: .../deploy-pm4.yml@develop`, `secrets: inherit`). + +When adding or tightening `if:` conditions, **think through all caller repo types** so shared CI stays green across the org. + +PHP packages have a composer.json and javascript packages have a package.json. Some have both. + +Processmaker core is the main consumer of all these packages. Its located at https://github.com/ProcessMaker/processmaker \ No newline at end of file diff --git a/.github/actions/common/Dockerfile.npm-run-prod b/.github/actions/common/Dockerfile.npm-run-prod new file mode 100644 index 0000000..4e30b0a --- /dev/null +++ b/.github/actions/common/Dockerfile.npm-run-prod @@ -0,0 +1,21 @@ +FROM node:24-alpine +ARG PM4_BRANCH=develop +ARG PM4_REPO=https://github.com/ProcessMaker/processmaker.git +RUN apk add git + +WORKDIR /repo + +RUN git clone --depth 1 --branch "${PM4_BRANCH}" --single-branch "${PM4_REPO}" . +RUN npx update-browserslist-db@latest +RUN npm install +# RUN NODE_OPTIONS="--max-old-space-size=6000" npm run prod --verbose + +RUN NODE_OPTIONS="--max-old-space-size=6000" npm run prod --verbose & \ + BUILD_PID=$!; \ + sleep 30; \ + echo "=== Memory after 30s ===" && free -h && ps aux | grep -i node; \ + sleep 60; \ + echo "=== Memory after 90s ===" && free -h && ps aux | grep -i node; \ + sleep 120; \ + echo "=== Memory after 210s ===" && free -h && ps aux | grep -i node; \ + wait $BUILD_PID \ No newline at end of file diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml index 7955c97..cd4d0bc 100644 --- a/.github/actions/common/action.yml +++ b/.github/actions/common/action.yml @@ -3,14 +3,26 @@ description: 'Here to dry up the workflow' inputs: token: - description: 'GitHub token' + description: 'GitHub token for git HTTPS and API (clone k8s repo, or sync merge when enabled)' required: true - + run_k8s_setup: + description: 'When true, resolve release branch and clone pm4-k8s-distribution' + required: false + default: 'true' + run_sync_merge_base: + description: 'When true, merge PR base into head when behind; may run npm run prod for public-only conflicts and push' + required: false + default: 'false' + package_path: + description: 'Path under workspace to the package git checkout (used by sync merge only)' + required: false + default: '.' runs: using: "composite" steps: - name: Determine the base branch + if: ${{ inputs.run_k8s_setup == 'true' }} run: | default_branch=${{ github.event.repository.default_branch }} base_branch=${{ github.event.pull_request.base.ref }} @@ -32,6 +44,7 @@ runs: shell: bash - name: Clone K8S + if: ${{ inputs.run_k8s_setup == 'true' }} run: | k8s_branch=$RELEASE_BRANCH @@ -44,4 +57,104 @@ runs: echo "*** k8s Branch: $k8s_branch ***" git clone --depth 1 -b "$k8s_branch" "https://${{ inputs.token }}@github.com/ProcessMaker/pm4-k8s-distribution.git" pm4-k8s-distribution echo "versionHelm=$(grep "version:" "pm4-k8s-distribution/charts/enterprise/Chart.yaml" | awk '{print $2}' | sed 's/\"//g')" >> $GITHUB_ENV - shell: bash \ No newline at end of file + shell: bash + + - name: Sync PR branch with base (merge when behind) + if: ${{ inputs.run_sync_merge_base == 'true' }} + env: + PACKAGE_PATH: ${{ inputs.package_path }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + set -euo pipefail + pkg="${PACKAGE_PATH:-.}" + cd "$pkg" + + if ! git rev-parse --git-dir >/dev/null 2>&1; then + echo "::error::Not a git repository at $pkg" + exit 1 + fi + + git_name="${GH_USER:-github-actions[bot]}" + git_email="${GH_EMAIL:-41898282+github-actions[bot]@users.noreply.github.com}" + git config user.name "$git_name" + git config user.email "$git_email" + + echo "Fetching origin/${BASE_REF}..." + git fetch origin "$BASE_REF" + + if git merge-base --is-ancestor "origin/${BASE_REF}" HEAD; then + echo "PR head already contains origin/${BASE_REF}; nothing to merge." + exit 0 + fi + + echo "PR head is missing commits from origin/${BASE_REF}; merging..." + set +e + git merge "origin/${BASE_REF}" --no-edit + merge_status=$? + set -e + + if [[ "$merge_status" -eq 0 ]]; then + echo "Merge completed without conflicts; pushing." + git push origin "HEAD:refs/heads/${HEAD_REF}" + exit 0 + fi + + echo "Merge reported conflicts." + conflicted=() + while IFS= read -r line; do + [[ -n "$line" ]] && conflicted+=("$line") + done < <(git diff --name-only --diff-filter=U || true) + + if [[ ${#conflicted[@]} -eq 0 ]]; then + echo "No unmerged paths listed; aborting merge." + git merge --abort || true + exit 0 + fi + + only_public=true + for f in "${conflicted[@]}"; do + case "$f" in + public|public/*) ;; + *) only_public=false; break ;; + esac + done + + if [[ "$only_public" != true ]]; then + echo "Conflicts exist outside public/; aborting merge and exiting without failure." + git merge --abort || true + exit 0 + fi + + has_prod=false + if [[ -f package.json ]] && node -e "const p=require('./package.json'); process.exit(p.scripts&&p.scripts.prod?0:1)"; then + has_prod=true + fi + + if [[ "$has_prod" != true ]]; then + echo "No npm run prod available; cannot auto-resolve public conflicts." + git merge --abort || true + exit 0 + fi + + set +e + npm run prod + prod_status=$? + set -e + if [[ "$prod_status" -ne 0 ]]; then + echo "npm run prod failed during conflict resolution; aborting merge." + git merge --abort || true + exit 0 + fi + + if git diff --name-only --diff-filter=U | grep -q .; then + echo "Conflicts remain after npm run prod; aborting merge." + git merge --abort || true + exit 0 + fi + + git add -A + git commit --no-edit || git commit -m "Merge origin/${BASE_REF} and rebuild public assets" + git push origin "HEAD:refs/heads/${HEAD_REF}" + echo "Pushed merge and rebuilt public assets." + shell: bash diff --git a/.github/workflows/deploy-pm4.yml b/.github/workflows/deploy-pm4.yml index 61137d9..3d6d129 100644 --- a/.github/workflows/deploy-pm4.yml +++ b/.github/workflows/deploy-pm4.yml @@ -29,6 +29,8 @@ env: GH_EMAIL: ${{ secrets.GH_EMAIL }} DOM_EKS: ${{ secrets.DOM_EKS }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} + # Git fetch/push for npm + sync-base job: point at another secret (e.g. GITHUB_TOKEN) when migrating off GIT_TOKEN. + PM4_SYNC_GIT_TOKEN: ${{ secrets.GIT_TOKEN }} BUILD_BASE: ${{ (contains(github.event.pull_request.body, 'ci:build-base') || github.event_name == 'schedule') && '1' || '0' }} MULTITENANCY: ${{ (contains(github.event.pull_request.body, 'ci:multitenancy')) && 'true' || 'false' }} BASE_IMAGE: ${{ secrets.REGISTRY_HOST }}/processmaker/processmaker:base @@ -41,6 +43,61 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.delete }} cancel-in-progress: true jobs: + npmInstallAndSyncBase: + name: npm install and sync PR with base + if: github.event_name == 'pull_request' && github.event.action != 'closed' && inputs.delete == '' + runs-on: ${{ vars.RUNNER }} + defaults: + run: + working-directory: repo + steps: + - name: Checkout package + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + path: repo + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + token: ${{ env.PM4_SYNC_GIT_TOKEN }} + persist-credentials: true + + - name: Checkout shared workflows (composite action) + uses: actions/checkout@v4 + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: _pm4_github + token: ${{ env.PM4_SYNC_GIT_TOKEN }} + + - name: npm install + prod (Docker) + env: + DOCKER_BUILDKIT: 1 + BUILDKIT_PROGRESS: plain + run: | + if [[ ! -f package.json ]]; then + echo "No package.json; skipping npm steps." + exit 0 + fi + set -euxo pipefail + docker version + docker info + free -h + df -h + docker build --progress=plain \ + --memory=12g \ + --cpu-shares=512 \ + --build-arg PM4_BRANCH=${{ env.CI_PACKAGE_BRANCH }} \ + -f "${GITHUB_WORKSPACE}/_pm4_github/.github/actions/common/Dockerfile.npm-run-prod" \ + . + + - name: Merge base when behind (optional public/ auto-resolve) + uses: ./_pm4_github/.github/actions/common + with: + token: ${{ env.PM4_SYNC_GIT_TOKEN }} + run_k8s_setup: 'false' + run_sync_merge_base: 'true' + package_path: repo + imageEKS: name: build-docker-image-EKS if: github.event.action != 'closed' && inputs.delete == '' @@ -70,18 +127,6 @@ jobs: - name: List Images run: | docker images - # - name: Run Trivy vulnerability scanner - # uses: aquasecurity/trivy-action@master - # with: - # image-ref: processmaker/enterprise:${{ env.VERSION }} - # format: 'table' - # exit-code: '0' - # ignore-unfixed: false - # vuln-type: 'os,library' - # scanners: 'vuln,secret' - # severity: 'MEDIUM,HIGH,CRITICAL' - # env: - # TRIVY_TIMEOUT: 30m - name: Login to Harbor uses: docker/login-action@v2 with: @@ -429,3 +474,40 @@ jobs: -Dsonar.tests=. -Dsonar.test.inclusions=**/*Test.php -Dsonar.php.coverage.reportPaths=./pm4-k8s-distribution/images/pm4-tools/coverage.xml + + trivyScan: + name: Trivy vulnerability scan + if: github.event.action != 'closed' && inputs.delete == '' + needs: imageEKS + runs-on: ${{ vars.RUNNER }} + steps: + - name: Set Image Tag + run: | + echo "RESOLVED_IMAGE_TAG=${{ env.IMAGE_TAG }}" >> $GITHUB_ENV + - name: Login to Harbor + uses: docker/login-action@v2 + with: + registry: ${{ secrets.REGISTRY_HOST }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: ${{ secrets.REGISTRY_HOST }}/processmaker/enterprise:${{ env.RESOLVED_IMAGE_TAG }} + format: json + output: trivy-results.json + exit-code: '0' + ignore-unfixed: false + vuln-type: os,library + scanners: vuln,secret + severity: MEDIUM,HIGH,CRITICAL + env: + TRIVY_TIMEOUT: 30m + + - name: Upload Trivy results + uses: actions/upload-artifact@v4 + with: + name: trivy-results-${{ github.run_id }} + path: trivy-results.json + if-no-files-found: warn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12e0b65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.envrc +docker