From 3a543d8a0a99a516dc9a7651ebee4e7ccc8a7bae Mon Sep 17 00:00:00 2001 From: Vito Albano Date: Tue, 5 May 2026 13:16:36 +0100 Subject: [PATCH] AAE-32905 Rework pr pipeline (#11853) * [AAE-32905] - Improve ADF workflow * [ci:force] * [AAE-32905] - Improve cache use * [AAE-32905] - Improve cache use * [AAE-32905] - Improved workflows * [AAE-32905] - Checking cache * [AAE-32905] - Checking cache * [AAE-32905] - Fix cli * [AAE-32905] - Fix cache * [AAE-32905] - Fix cache * [AAE-32905] - Fix cache * [AAE-32905] - improving workflow and adding some storybook tests * [AAE-32905] - improving workflow and adding some storybook tests * [AAE-32905] - Added more improvements * [AAE-32905] - Added more improvements * [AAE-32905] - bugfix * [AAE-32905] - Added missing part to the cache * [AAE-32905] - bugfix * [AAE-32905] - fixed cache poisoning * [AAE-32905] - timing jobs * [AAE-32905] - chosen to build all together as it's more efficient * [AAE-32905] - Fixed script injection issue --- .github/CODEOWNERS | 4 +- .github/actions/save-nx-cache/action.yml | 17 + .github/actions/setup/action.yml | 49 ++- .github/workflows/build-lib-workflow.yml | 42 ++ .github/workflows/pull-request.yml | 400 ++++++++++-------- .github/workflows/unit-test-workflow.yml | 87 ++++ docs/ci-commands.md | 62 +++ lib/cli/project.json | 2 + .../card-view-boolitem.component.stories.ts | 11 + .../card-view-textitem.component.stories.ts | 19 + .../comments/comments.component.stories.ts | 21 +- .../pagination.component.stories.ts | 18 +- .../lib/toolbar/toolbar.component.stories.ts | 9 +- lib/js-api/project.json | 1 + nx.json | 37 +- 15 files changed, 567 insertions(+), 212 deletions(-) create mode 100644 .github/actions/save-nx-cache/action.yml create mode 100644 .github/workflows/build-lib-workflow.yml create mode 100644 .github/workflows/unit-test-workflow.yml create mode 100644 docs/ci-commands.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 596d43ee2a..ffeb1ca20e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,9 +10,9 @@ # precedence. When someone opens a pull request that only # modifies JS files, only @js-owner and not the global # owner(s) will be requested for a review. -/e2e/ @eromano @cristinaj +/e2e/ @eromano # The `docs/*` pattern will match files like # `docs/getting-started.md` but not further nested files like # `docs/build-app/troubleshooting.md`. -/docs/ @m-hulbert @eromano +/docs/ @eromano diff --git a/.github/actions/save-nx-cache/action.yml b/.github/actions/save-nx-cache/action.yml new file mode 100644 index 0000000000..ffe56b503d --- /dev/null +++ b/.github/actions/save-nx-cache/action.yml @@ -0,0 +1,17 @@ +name: 'Save Nx Cache' +description: 'Save Nx cache and dist outputs only when the job succeeds' +inputs: + cache-suffix: + description: 'Must match the cache-suffix used in the setup action' + required: true +runs: + using: "composite" + steps: + - name: Save nx cache + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + nxcache + .nx + dist + key: ${{ runner.os }}-nxcache-${{ inputs.cache-suffix }}-${{ hashFiles('nx.json') }}-${{ github.sha }} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 27675fdcfa..e82100d7fd 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -6,6 +6,15 @@ inputs: required: false type: boolean default: 'true' + cache-suffix: + description: 'Suffix to make Nx cache key unique per job (e.g. matrix project name)' + required: false + default: 'default' + full-setup: + description: 'Run git-latest-tag, npm-tag, and before-install (requires fetch-depth: 0). Set to false for matrix jobs that only need node/cache.' + required: false + type: boolean + default: 'true' act: description: 'enable act debug' required: false @@ -15,36 +24,50 @@ outputs: npm-tag: description: 'NPM tag' value: ${{ steps.set-npm-tag.outputs.npm-tag }} + node-modules-cache-hit: + description: 'Whether node_modules cache was hit' + value: ${{ steps.node-modules-cache.outputs.cache-hit }} runs: using: "composite" steps: + - name: Set consistent machine ID for Nx cache + shell: bash + run: echo "nx-github-actions" | sudo tee /etc/machine-id > /dev/null - name: install NPM uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: '.nvmrc' cache-dependency-path: package-lock.json - name: get latest tag sha + if: ${{ inputs.full-setup == 'true' }} id: tag-sha - uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@f95467b62527ce087c9a1b7e55c467061cf827a0 # v17.6.1 -# CACHE - - name: Node Modules cache + uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@8d76614b374e521d792b4a1c9827409960b66bae # v17.6.0 + - name: load "NPM TAG" + if: ${{ inputs.full-setup == 'true' }} + id: set-npm-tag + uses: ./.github/actions/set-npm-tag + - name: Cache node modules id: node-modules-cache if: ${{ inputs.enable-node-modules-cache == 'true' }} uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - env: - cache-name: node-modules-cache + with: + path: node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('.nvmrc') }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-modules-${{ hashFiles('.nvmrc') }}- + - name: Restore nx cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | - node_modules - key: .npm-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + nxcache + .nx + dist + key: ${{ runner.os }}-nxcache-${{ inputs.cache-suffix }}-${{ hashFiles('nx.json') }}-${{ github.sha }} restore-keys: | - .npm-${{ runner.os }}-build-${{ env.cache-name }}- - .npm-${{ runner.os }}-build- - .npm-${{ runner.os }}- - - name: load "NPM TAG" - id: set-npm-tag - uses: ./.github/actions/set-npm-tag + ${{ runner.os }}-nxcache-${{ inputs.cache-suffix }}-${{ hashFiles('nx.json') }}- + ${{ runner.os }}-nxcache-${{ inputs.cache-suffix }}- - name: before install script + if: ${{ inputs.full-setup == 'true' }} uses: ./.github/actions/before-install with: act: ${{ inputs.act }} diff --git a/.github/workflows/build-lib-workflow.yml b/.github/workflows/build-lib-workflow.yml new file mode 100644 index 0000000000..aca14a6516 --- /dev/null +++ b/.github/workflows/build-lib-workflow.yml @@ -0,0 +1,42 @@ +name: "Build Lib Workflow" + +on: + workflow_call: + inputs: + base_ref: + description: 'Base branch for affected calculation' + required: false + type: string + default: 'develop' + +env: + NODE_OPTIONS: "--max-old-space-size=5120" + +jobs: + build: + name: "Build affected libs" + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup + with: + cache-suffix: build + full-setup: 'false' + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Build affected libs + env: + BASE_REF: ${{ inputs.base_ref }} + run: npx nx affected --target=build --base=origin/$BASE_REF --head=HEAD --configuration=production --exclude=stories + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache + with: + cache-suffix: build diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b8b54be914..b5ea0d330e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -31,16 +31,14 @@ concurrency: cancel-in-progress: true env: - BASE_REF: ${{ github.base_ref }} - HEAD_REF: ${{ github.head_ref }} GH_COMMIT: ${{ github.sha }} - GH_BUILD_NUMBER: ${{ github.run_id }} - LOG_LEVEL: "ERROR" NODE_OPTIONS: "--max-old-space-size=5120" jobs: pre-checks: runs-on: ubuntu-latest + outputs: + code-changed: ${{ steps.path-filter.outputs.code-changed }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -57,150 +55,121 @@ jobs: exit 1 fi + - name: Detect code changes + id: path-filter + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "Not a PR event — assuming code changed" + echo "code-changed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + FILES=$(gh api /repos/$GITHUB_REPOSITORY/pulls/${{ github.event.pull_request.number }}/files --paginate --jq '.[].filename') + + CODE_CHANGED="false" + while IFS= read -r file; do + case "$file" in + *.md|docs/*|.github/*.md|.github/CODEOWNERS|.github/dependabot.yml|LICENSE*|NOTICE*|.editorconfig|.gitattributes) + ;; + *) + CODE_CHANGED="true" + break + ;; + esac + done <<< "$FILES" + + echo "code-changed=$CODE_CHANGED" >> $GITHUB_OUTPUT + echo "Code changed: $CODE_CHANGED" + check-if-pr-is-approved: runs-on: ubuntu-latest + steps: + - name: Check if PR approval can be skipped + id: skip-check + env: + EVENT_NAME: ${{ github.event_name }} + ACTOR: ${{ github.actor }} + DEVEL_FLAG: ${{ inputs.devel }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message || github.event.pull_request.title || '' }} + run: | + skip="false" + + if [ "$EVENT_NAME" == "schedule" ] || [ "$EVENT_NAME" == "workflow_dispatch" ]; then + echo -e "\033[32mSchedule/dispatch event — skipping approval check\033[0m" + skip="true" + elif [ "$ACTOR" == "dependabot[bot]" ] || [ "$ACTOR" == "alfresco-build" ]; then + echo -e "\033[32mCommit by $ACTOR — skipping approval check\033[0m" + skip="true" + elif [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then + echo -e "\033[32m[ci:force] flag detected — skipping approval check\033[0m" + skip="true" + elif [[ "$DEVEL_FLAG" == "true" ]]; then + echo -e "\033[32mDevel flag — skipping approval check\033[0m" + skip="true" + fi + + echo "skip=$skip" >> $GITHUB_OUTPUT + + - name: Get PR number + if: ${{ steps.skip-check.outputs.skip != 'true' }} + id: pr-number + env: + GH_TOKEN: ${{ github.token }} + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr view --json number --jq '.number' 2>/dev/null || echo "") + fi + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "PR: $PR_NUMBER" + + - name: Check if PR is approved + if: ${{ steps.skip-check.outputs.skip != 'true' && steps.pr-number.outputs.pr_number != '' }} + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.pr-number.outputs.pr_number }} + run: | + echo "Checking approval for PR: $PR_NUMBER" + checkApproval=$(gh api /repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews | jq '.[] | select(.state == "APPROVED") | .user.login') + if [[ $checkApproval ]]; then + echo -e "\033[32mPR approved\033[0m" + else + echo -e "\033[31mPR NOT approved\033[0m" + exit 1 + fi + + setup: + timeout-minutes: 30 + name: "Setup" + runs-on: ubuntu-latest + needs: [check-if-pr-is-approved, pre-checks] + if: ${{ needs.pre-checks.outputs.code-changed == 'true' }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - - - name: Get branch name - uses: Alfresco/alfresco-build-tools/.github/actions/get-branch-name@f95467b62527ce087c9a1b7e55c467061cf827a0 # v17.6.1 - - - name: Save commit message - uses: Alfresco/alfresco-build-tools/.github/actions/get-commit-message@f95467b62527ce087c9a1b7e55c467061cf827a0 # v17.6.1 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup with: - header-only: true - - - name: ci:force flag parser - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - ACTOR: ${{ github.actor }} + cache-suffix: setup + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Bundle run: | - if [ "$EVENT_NAME" == "schedule" ] || [ "$ACTOR" == "dependabot[bot]" ]; then - echo -e "\033[32mci:force check can be skipped\033[0m" - skip_check="true" - elif [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then - echo -e "\033[32m[ci:force] flag detected. No need for approval.\033[0m" - skip_check="true" - fi - - - name: Get PR number - if: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' }} - uses: kamatama41/get-pr-number-action@ff143e03abaa79d346ca5407d49d335b5def979e # v1.0.1 - id: action - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: show pr number - shell: bash - env: - PR_NUMBER: ${{ steps.action.outputs.number }} - run: | - echo "PR: $PR_NUMBER" - - - name: check if pr is approved - env: - DEVEL_FLAG: ${{ inputs.devel }} - GH_TOKEN: ${{ github.token }} - skip_check: "false" - EVENT_NAME: ${{ github.event_name }} - ACTOR: ${{ github.actor }} - PR_NUMBER: ${{ steps.action.outputs.number }} - run: | - if [ "$EVENT_NAME" == "schedule" ] || [ "$ACTOR" == "dependabot[bot]" ]; then - echo -e "\033[32mci:force check can be skipped\033[0m" - skip_check="true" - elif [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then - echo -e "\033[32m[ci:force] flag detected. No need for approval.\033[0m" - skip_check="true" - fi - - if [ "$ACTOR" == "dependabot[bot]" ] || [ "$ACTOR" == "alfresco-build" ]; then - echo -e "\033[32mCommit by $ACTOR. No need for approval.\033[0m" - skip_check="true" - fi - if [ "$EVENT_NAME" == "schedule" ] || [ "$EVENT_NAME" == "workflow_dispatch" ]; then - echo -e "\033[32mSchedule event\033[0m" - skip_check="true" - fi - - if [[ "$DEVEL_FLAG" == "true" ]]; then - echo -e "\033[32mDevel flag\033[0m" - skip_check="true" - fi - - if [ "$skip_check" == "false" ]; then - echo "Checking PR approval" - prNumber=$PR_NUMBER - echo "PR: $prNumber" - checkApproval=$(gh api /repos/$GITHUB_REPOSITORY/pulls/$prNumber/reviews | jq '.[] | select(.state == "APPROVED") | .user.login') - if [[ $checkApproval ]]; then - echo -e "\033[32mPR approved\033[0m" - else - echo -e "\033[31mPR NOT approved\033[0m" - exit 1 - fi - fi - - setup: - # long timeout required when cache has to be recreated - timeout-minutes: 30 - name: "Setup" - runs-on: ubuntu-latest - needs: [check-if-pr-is-approved, pre-checks] - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # Fetch all history for all tags and branches - - uses: ./.github/actions/setup - - name: install - run: | - npm ci npm run bundle:js-api npm run bundle:cli - - uses: ./.github/actions/upload-node-modules-and-artifacts - - unit-tests: - timeout-minutes: 30 - name: "Unit tests: ${{ matrix.unit-tests.name }}" - runs-on: ubuntu-latest - needs: [setup] - strategy: - fail-fast: false - # max-parallel: 4 - matrix: - unit-tests: - - name: js-api - exclude: "core,insights,content-services,process-services,process-services-cloud,eslint-plugin-eslint-angular" - - name: content-services - exclude: "insights,core,extensions,process-services,process-services-cloud,eslint-plugin-eslint-angular,js-api" - - name: core - exclude: "insights,content-services,process-services,process-services-cloud,eslint-plugin-eslint-angular,js-api" - - name: insights - exclude: "core,extensions,content-services,process-services-cloud,process-services,eslint-plugin-eslint-angular,js-api" - - name: process-services - exclude: "core,extensions,content-services,process-services-cloud,insights,eslint-plugin-eslint-angular,js-api" - - name: process-services-cloud - exclude: "insights,core,extensions,content-services,process-services$,eslint-plugin-eslint-angular,js-api" - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache with: - fetch-depth: 0 # Fetch all history for all tags and branches - - uses: ./.github/actions/setup - - uses: ./.github/actions/download-node-modules-and-artifacts - - name: Run unit tests - env: - EXCLUDE_PATTERN: ${{ matrix.unit-tests.exclude }} - run: | - /usr/bin/xvfb-run --auto-servernum npm run test:affected -- $NX_CALCULATION_FLAGS --exclude=$EXCLUDE_PATTERN + cache-suffix: setup lint: - # long timeout required when cache has to be recreated timeout-minutes: 30 name: "Lint" runs-on: ubuntu-latest @@ -209,71 +178,133 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 # Fetch all history for all tags and branches - - uses: ./.github/actions/setup - - uses: ./.github/actions/download-node-modules-and-artifacts - - run: npm run lint:affected -- $NX_CALCULATION_FLAGS - - build-libs: - # long timeout required when cache has to be recreated - timeout-minutes: 30 - name: "Build libs" - runs-on: ubuntu-latest - needs: [setup] - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + fetch-depth: 0 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup with: - fetch-depth: 0 # Fetch all history for all tags and branches - - uses: ./.github/actions/setup - - uses: ./.github/actions/download-node-modules-and-artifacts - - run: NX_REJECT_UNKNOWN_LOCAL_CACHE=0 npm run build:affected -- $NX_CALCULATION_FLAGS --prod - - uses: ./.github/actions/upload-node-modules-and-artifacts + cache-suffix: lint + full-setup: 'false' + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Run lint + env: + BASE_REF: ${{ github.base_ref || 'develop' }} + run: npx nx affected --target=lint --base=origin/$BASE_REF --head=HEAD + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache + with: + cache-suffix: lint + + trigger-build: + name: "Build Libs" + needs: [setup] + uses: ./.github/workflows/build-lib-workflow.yml + with: + base_ref: ${{ github.base_ref || 'develop' }} build-storybook: timeout-minutes: 30 - name: "Build storybook" - runs-on: ubuntu-latest + name: "Build Storybook" needs: [setup] + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 # Fetch all history for all tags and branches - - uses: ./.github/actions/setup - - uses: ./.github/actions/download-node-modules-and-artifacts - - run: npm run build-storybook - - uses: ./.github/actions/upload-node-modules-and-artifacts + fetch-depth: 0 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup + with: + cache-suffix: storybook + full-setup: 'false' + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Build Storybook + env: + BASE_REF: ${{ github.base_ref || 'develop' }} + run: | + npx nx affected --target=build-storybook --base=origin/$BASE_REF --head=HEAD --configuration=ci + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache + with: + cache-suffix: storybook + + trigger-unit-tests: + name: "Unit Tests" + needs: [setup] + uses: ./.github/workflows/unit-test-workflow.yml + with: + base_ref: ${{ github.base_ref || 'develop' }} + + PR-size-check: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Check PR size + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const additions = pr.additions; + const deletions = pr.deletions; + const totalChanges = additions + deletions; + const changedFiles = pr.changed_files; + + let size = 'S'; + if (totalChanges > 1000 || changedFiles > 30) size = 'XL'; + else if (totalChanges > 500 || changedFiles > 20) size = 'L'; + else if (totalChanges > 200 || changedFiles > 10) size = 'M'; + + core.summary + .addHeading('PR Size: ' + size, 3) + .addTable([ + [{data: 'Metric', header: true}, {data: 'Count', header: true}], + ['Files changed', String(changedFiles)], + ['Additions', '+' + String(additions)], + ['Deletions', '-' + String(deletions)], + ['Total changes', String(totalChanges)], + ]); + + if (size === 'XL') { + core.summary.addRaw('⚠️ This PR is very large. Consider splitting it into smaller PRs for easier review.'); + } + + await core.summary.write(); + + if (size === 'XL') { + core.warning('This PR has ' + totalChanges + ' changes across ' + changedFiles + ' files. Consider splitting it for easier review.'); + } PR-forbidden-labels: if: ${{ inputs.cron-run == '' || inputs.cron-run == 'false' }} runs-on: ubuntu-latest steps: - - id: checkoutRepo - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - - name: PR contains forbidden labels - id: pr-forbidden + - name: Check for forbidden labels uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const issueHasLabels = require('./scripts/github/update/check-issue-has-label.js'); - const checkLabels = ['next version ➡️', 'do not merge🙅🏻‍♂️']; + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const labels = issue.labels?.map(item => item.name) || []; + const forbidden = ['next version ➡️', 'do not merge🙅🏻‍♂️']; - const hasLabel = await issueHasLabels({github, context, checkLabels}) - - if(hasLabel) { + if (forbidden.some(l => labels.includes(l))) { core.setFailed('The PR contains a forbidden label! You are not allowed to merge until the label is there.'); } - - name: Check value after - env: - STEP_RESULT: ${{ toJson(steps.pr-forbidden.*.result) }} - HAS_FAILURE: ${{ contains(toJson(steps.pr-forbidden.*.result), 'failure') }} - run: | - echo "Step result: $STEP_RESULT" - echo "Has failure: $HAS_FAILURE" finalize: if: ${{ always() }} @@ -284,16 +315,13 @@ jobs: check-if-pr-is-approved, pre-checks, setup, - unit-tests, + trigger-unit-tests, lint, - build-libs, + trigger-build, build-storybook, + PR-forbidden-labels, ] steps: - name: Check job execution status - if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - }} + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} run: exit 1 diff --git a/.github/workflows/unit-test-workflow.yml b/.github/workflows/unit-test-workflow.yml new file mode 100644 index 0000000000..9f182d01d5 --- /dev/null +++ b/.github/workflows/unit-test-workflow.yml @@ -0,0 +1,87 @@ +name: "Unit Tests Workflow" + +on: + workflow_call: + inputs: + base_ref: + description: 'Base branch for affected calculation' + required: false + type: string + default: 'develop' + +jobs: + generate-affected-matrix: + name: "Generate affected matrix" + runs-on: ubuntu-latest + outputs: + unitMatrix: ${{ steps.set-matrix.outputs.unitMatrix }} + hasProjects: ${{ steps.set-matrix.outputs.hasProjects }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup + with: + cache-suffix: test-matrix + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Generate affected projects matrix + id: set-matrix + env: + BASE_REF: ${{ inputs.base_ref }} + run: | + echo "Base ref is $BASE_REF" + AFFECTED_UNIT=$(npx nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular) + echo "Affected projects for UNIT: $AFFECTED_UNIT" + + if [ -z "$AFFECTED_UNIT" ]; then + echo "No affected projects found" + echo "hasProjects=false" >> $GITHUB_OUTPUT + echo "unitMatrix=[]" >> $GITHUB_OUTPUT + else + UNIT_MATRIX_JSON=$(echo "$AFFECTED_UNIT" | xargs -n1 | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({ "project": . })') + echo "Matrix UNIT: $UNIT_MATRIX_JSON" + echo "hasProjects=true" >> $GITHUB_OUTPUT + echo "unitMatrix=$UNIT_MATRIX_JSON" >> $GITHUB_OUTPUT + fi + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache + with: + cache-suffix: test-matrix + + unit-tests: + timeout-minutes: 30 + runs-on: ubuntu-latest + needs: generate-affected-matrix + if: ${{ needs.generate-affected-matrix.outputs.hasProjects == 'true' }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.generate-affected-matrix.outputs.unitMatrix) }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup environment + id: setup-env + uses: ./.github/actions/setup + with: + cache-suffix: test-${{ matrix.project }} + full-setup: 'false' + - name: Install dependencies + if: ${{ steps.setup-env.outputs.node-modules-cache-hit != 'true' }} + run: npm ci + - name: Run unit tests for ${{ matrix.project }} + env: + NODE_OPTIONS: "--max-old-space-size=5120" + run: | + xvfb-run --auto-servernum npx nx run ${{ matrix.project }}:test + - name: Save nx cache + if: ${{ success() }} + uses: ./.github/actions/save-nx-cache + with: + cache-suffix: test-${{ matrix.project }} diff --git a/docs/ci-commands.md b/docs/ci-commands.md new file mode 100644 index 0000000000..3437114504 --- /dev/null +++ b/docs/ci-commands.md @@ -0,0 +1,62 @@ +# CI Commands and Labels + +Reference for flags and labels that control CI behavior on pull requests. + +## PR Title Flags + +Include these flags anywhere in the **PR title** to modify CI behavior. + +| Flag | Effect | +|------|--------| +| `[ci:force]` | Skips the PR approval check, allowing the full pipeline to run without an approved review. Useful for testing CI changes on draft PRs. | + +Example: `[AAE-12345] Fix pagination component [ci:force]` + +## Commit Message Flags + +Include these flags in a **commit message** to modify how Nx calculates affected projects. + +| Flag | Effect | +|------|--------| +| `[affected:*]` | Forces all Nx targets to run against all projects, ignoring the affected calculation. Equivalent to `--all`. | + +## PR Labels + +Labels applied to the PR that affect CI behavior. + +| Label | Effect | +|-------|--------| +| `do not merge` | Blocks the PR from passing the finalize check. CI will fail until the label is removed. | +| `next version` | Same as above - blocks the PR from merging. Used to hold PRs for the next release cycle. | +| `hxp-upstream` | (Release only) Triggers the HxP upstream propagation workflow after a successful release. | + +## Automatic Skip Conditions + +The approval check is automatically skipped for: + +- **Scheduled runs** (`schedule` event) +- **Workflow dispatch** (`workflow_dispatch` event) +- **Dependabot PRs** (actor is `dependabot[bot]`) +- **Build bot PRs** (actor is `alfresco-build`) +- **Devel flag** (when `devel: true` is passed via `workflow_call`) + +## Path-Based Filtering + +PRs that only modify non-code files skip the build, test, and lint jobs entirely. Files considered non-code: + +- Markdown files (`*.md`) +- Documentation (`docs/`) +- `LICENSE`, `NOTICE` +- `.editorconfig`, `.gitattributes` +- `.github/CODEOWNERS`, `.github/dependabot.yml` + +## PR Size Check + +Every PR gets an automatic size classification displayed in the job summary: + +| Size | Criteria | +|------|----------| +| **S** | < 200 total changes and < 10 files | +| **M** | 200-500 total changes or 10-20 files | +| **L** | 500-1000 total changes or 20-30 files | +| **XL** | > 1000 total changes or > 30 files (warning annotation added) | diff --git a/lib/cli/project.json b/lib/cli/project.json index 5616b07ffb..c83a1ed230 100644 --- a/lib/cli/project.json +++ b/lib/cli/project.json @@ -7,6 +7,7 @@ "targets": { "build": { "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/dist/libs/cli"], "options": { "command": "cd lib/cli && npm run dist" }, @@ -30,6 +31,7 @@ }, "copyToNodeModules": { "executor": "nx:run-commands", + "cache": false, "options": { "commands": [ { diff --git a/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.stories.ts b/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.stories.ts index 2b0f9d4dd4..8b554f4d31 100644 --- a/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.stories.ts +++ b/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.stories.ts @@ -19,6 +19,7 @@ import { Meta, StoryObj } from '@storybook/angular'; import { CardViewBoolItemComponent } from './card-view-boolitem.component'; import { CardViewBoolItemModel } from '../../public-api'; import { cardViewSharedMeta } from '../../stories/card-view-shared-meta'; +import { expect, within } from 'storybook/test'; const meta: Meta = { ...cardViewSharedMeta, @@ -50,5 +51,15 @@ export const CardViewBoolItem: Story = { default: false, editable: true }) + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const label = canvas.getByText(/Agree to all terms and conditions/i); + await expect(label).toBeVisible(); + + const toggle = canvas.getByRole('checkbox'); + await expect(toggle).toBeVisible(); + await expect(toggle).toBeChecked(); } }; diff --git a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.stories.ts b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.stories.ts index 76ec338c91..6fe6374248 100644 --- a/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.stories.ts +++ b/lib/core/src/lib/card-view/components/card-view-textitem/card-view-textitem.component.stories.ts @@ -19,6 +19,7 @@ import { Meta, StoryObj } from '@storybook/angular'; import { CardViewTextItemComponent } from './card-view-textitem.component'; import { CardViewTextItemModel } from '../../public-api'; import { cardViewSharedMeta } from '../../stories/card-view-shared-meta'; +import { expect, within } from 'storybook/test'; const meta: Meta = { ...cardViewSharedMeta, @@ -50,6 +51,15 @@ export const ClickableCardViewTextItem: Story = { clickable: true, icon: 'close' }) + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const label = canvas.getByText(/CardView Text Item - Clickable template/i); + await expect(label).toBeVisible(); + + const value = canvas.getByText(/click here/i); + await expect(value).toBeVisible(); } }; @@ -104,5 +114,14 @@ export const DefaultCardViewTextItem: Story = { icon: 'close', multiline: false }) + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const label = canvas.getByText(/CardView Text Item - Default template/i); + await expect(label).toBeVisible(); + + const value = canvas.getByText(/input here/i); + await expect(value).toBeVisible(); } }; diff --git a/lib/core/src/lib/comments/comments.component.stories.ts b/lib/core/src/lib/comments/comments.component.stories.ts index 205a546bc0..70885215a6 100644 --- a/lib/core/src/lib/comments/comments.component.stories.ts +++ b/lib/core/src/lib/comments/comments.component.stories.ts @@ -21,6 +21,7 @@ import { ADF_COMMENTS_SERVICE } from './interfaces/comments.token'; import { commentsStoriesData } from './mocks/comments.stories.mock'; import { CommentsServiceStoriesMock } from './mocks/comments.service.stories.mock'; import { provideStoryCore } from '../stories/core-story.providers'; +import { expect, within } from 'storybook/test'; const meta: Meta = { component: CommentsComponent, @@ -92,6 +93,12 @@ export const SingleCommentWithAvatar: Story = { args: { comments: [commentsStoriesData[0]], readOnly: true + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const commentText = canvas.getByText(/I've done this task, what's next\?/i); + await expect(commentText).toBeVisible(); } }; @@ -112,11 +119,23 @@ export const NoComments: Story = { args: { comments: [], readOnly: true + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const commentElements = canvas.queryAllByText(/I've done this task/i); + await expect(commentElements.length).toBe(0); } }; export const Comments: Story = { render: (args) => ({ props: args - }) + }), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const commentInput = canvas.getByRole('textbox'); + await expect(commentInput).toBeVisible(); + } }; diff --git a/lib/core/src/lib/pagination/pagination.component.stories.ts b/lib/core/src/lib/pagination/pagination.component.stories.ts index 235e1cb3f1..b9e72e30f9 100644 --- a/lib/core/src/lib/pagination/pagination.component.stories.ts +++ b/lib/core/src/lib/pagination/pagination.component.stories.ts @@ -18,6 +18,7 @@ import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular'; import { PaginationComponent } from './pagination.component'; import { provideStoryCore } from '../stories/core-story.providers'; +import { expect, within } from 'storybook/test'; const meta: Meta = { component: PaginationComponent, @@ -95,5 +96,20 @@ type Story = StoryObj; export const Pagination: Story = { render: (args) => ({ props: args - }) + }), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const paginationRange = canvas.getByText(/1-25 of 100/i); + await expect(paginationRange).toBeVisible(); + + const pageSizeSelector = canvas.getByRole('combobox'); + await expect(pageSizeSelector).toBeVisible(); + + const nextButton = canvas.getByRole('button', { name: /next/i }); + await expect(nextButton).toBeVisible(); + + const previousButton = canvas.getByRole('button', { name: /previous/i }); + await expect(previousButton).toBeVisible(); + } }; diff --git a/lib/core/src/lib/toolbar/toolbar.component.stories.ts b/lib/core/src/lib/toolbar/toolbar.component.stories.ts index c7df9a1002..48134f2479 100644 --- a/lib/core/src/lib/toolbar/toolbar.component.stories.ts +++ b/lib/core/src/lib/toolbar/toolbar.component.stories.ts @@ -20,6 +20,7 @@ import { ToolbarComponent } from './toolbar.component'; import { ToolbarDividerComponent } from './toolbar-divider.component'; import { ToolbarTitleComponent } from './toolbar-title.component'; import { provideStoryCore } from '../stories/core-story.providers'; +import { expect, within } from 'storybook/test'; type ToolbarStoryArgs = ToolbarComponent & { toolbarTitle?: boolean; @@ -118,5 +119,11 @@ export const Toolbar: Story = { leftright ` - }) + }), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const toolbar = canvas.getByRole('toolbar'); + await expect(toolbar).toBeVisible(); + } }; diff --git a/lib/js-api/project.json b/lib/js-api/project.json index 53a77f63a0..2ee8bc58bb 100644 --- a/lib/js-api/project.json +++ b/lib/js-api/project.json @@ -95,6 +95,7 @@ }, "copyToNodeModules": { "executor": "nx:run-commands", + "cache": false, "options": { "commands": [ { diff --git a/nx.json b/nx.json index 7a6cab8eb1..8edad9a277 100644 --- a/nx.json +++ b/nx.json @@ -14,14 +14,35 @@ }, "stylelint": { "cache": true - } - }, - "tasksRunnerOptions": { - "default": { - "runner": "nx/tasks-runners/default", - "options": { - "runtimeCacheInputs": ["node -v"] - } + }, + "build-storybook": { + "cache": true, + "inputs": ["default", "^production"], + "outputs": ["{options.outputDir}"] + }, + "build-cjs": { + "cache": true, + "inputs": ["production", "^production"] + }, + "build-esm2015": { + "cache": true, + "inputs": ["production", "^production"] + }, + "build-esm5": { + "cache": true, + "inputs": ["production", "^production"] + }, + "build-types": { + "cache": true, + "inputs": ["production", "^production"] + }, + "build-schematics": { + "cache": true, + "inputs": ["production", "^production"] + }, + "license": { + "cache": true, + "outputs": ["{workspaceRoot}/licenses.txt"] } }, "namedInputs": {