diff --git a/.github/scripts/list_workspaces.js b/.github/scripts/list_workspaces.js new file mode 100644 index 0000000000..ffed1533a9 --- /dev/null +++ b/.github/scripts/list_workspaces.js @@ -0,0 +1,85 @@ +/** + * Lists the workspaces a CI matrix should cover, derived from the root + * `package.json` and each workspace's own files, so that adding a package to + * the root `workspaces` array is all it takes to get it into CI. + * + * Usage: node list_workspaces.js + * + * unit every non-private workspace under `packages/`; the other workspaces + * (layers, examples, testing utils) have dedicated jobs + * e2e every workspace with `tests/e2e/*.test.ts` files other than LMI ones + * e2e-lmi every workspace with `tests/e2e/lmi*.test.ts` files + * + * A workspace with e2e suites must also define the script the matrix job runs + * (`test:e2e`, or `test:e2e:lmi` for LMI suites), otherwise this fails loudly + * instead of letting the job fail on a missing script. + * + * Prints a sorted JSON array of workspace paths, ready for `fromJSON()` in a + * matrix definition. + */ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const rootDir = join(import.meta.dirname, '..', '..'); + +const readManifest = (workspace) => + JSON.parse(readFileSync(join(rootDir, workspace, 'package.json'), 'utf-8')); + +const e2eTestFiles = (workspace) => { + const dir = join(rootDir, workspace, 'tests', 'e2e'); + if (!existsSync(dir)) return []; + return readdirSync(dir).filter((file) => file.endsWith('.test.ts')); +}; + +/** LMI suites run in their own job, see `test:e2e:lmi` in the package scripts. */ +const isLmiTest = (file) => file.startsWith('lmi.'); + +const fail = (message) => { + console.error(message); + process.exit(1); +}; + +const hasE2eSuites = (workspace, matches, script) => { + const hasSuites = e2eTestFiles(workspace).some(matches); + if (hasSuites && readManifest(workspace).scripts?.[script] === undefined) { + fail(`${workspace} has e2e suites but no "${script}" script to run them`); + } + return hasSuites; +}; + +const selectors = { + unit: (workspace) => + workspace.startsWith('packages/') && + readManifest(workspace).private !== true, + e2e: (workspace) => + hasE2eSuites(workspace, (file) => !isLmiTest(file), 'test:e2e'), + 'e2e-lmi': (workspace) => hasE2eSuites(workspace, isLmiTest, 'test:e2e:lmi'), +}; + +const [mode] = process.argv.slice(2); +const select = selectors[mode]; +if (select === undefined) { + console.error( + `Usage: node list_workspaces.js <${Object.keys(selectors).join('|')}>` + ); + process.exit(1); +} + +const { workspaces } = readManifest('.'); +const selected = []; +for (const workspace of workspaces) { + if (workspace.includes('*')) { + console.error( + `Workspace pattern "${workspace}" is a glob; list every workspace explicitly in the root package.json` + ); + process.exit(1); + } + if (select(workspace)) selected.push(workspace); +} + +if (selected.length === 0) { + console.error(`No workspaces matched mode "${mode}"`); + process.exit(1); +} + +console.log(JSON.stringify(selected.toSorted())); diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index ea120fae55..eda09820ec 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -2,109 +2,17 @@ name: Code quality # USAGE # -# Automatically triggered when a PR is added to the merge queue. +# Automatically triggered when a PR is added to the merge queue. Runs the same linting +# and unit test jobs as the PR workflow, see ./.github/workflows/reusable-run-linting-check-and-unit-tests.yml. on: merge_group: branches: - main -permissions: {} +permissions: + contents: read jobs: - code-quality: - permissions: - contents: read - runs-on: ubuntu-latest - env: - NODE_ENV: dev - strategy: - matrix: - version: [22, 24] - workspace: [ - "packages/batch", - "packages/commons", - "packages/event-handler", - "packages/idempotency", - "packages/jmespath", - "packages/logger", - "packages/tracer", - "packages/parser", - "packages/parameters", - "packages/metrics" - ] - fail-fast: false - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup NodeJS - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ matrix.version }} - cache: "npm" - - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - with: - nodeVersion: ${{ matrix.version }} - - name: Linting - run: npm run lint -w ${{ matrix.workspace }} - - name: Unit tests - run: | - npm run test:unit:coverage -w ${{ matrix.workspace }} - npm run test:unit:types -w ${{ matrix.workspace }} - check-layer-publisher: - permissions: - contents: read - runs-on: ubuntu-latest - env: - NODE_ENV: dev - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup NodeJS - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24 - cache: "npm" - - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - - name: Run linting - run: npm run lint -w layers - - name: Run tests - run: npm run test:unit -w layers - check-docs-snippets: - permissions: - contents: read - runs-on: ubuntu-latest - env: - NODE_ENV: dev - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup NodeJS - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24 - cache: "npm" - - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - - name: Run linting - run: npm run lint -w examples/snippets - check-docs: - permissions: - contents: read - runs-on: ubuntu-latest - env: - NODE_ENV: dev - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup NodeJS - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24 - cache: "npm" - - name: Setup dependencies - uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - - name: Run linting - run: npm run lint:markdown + run-unit-tests: + uses: ./.github/workflows/reusable-run-linting-check-and-unit-tests.yml diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index 2e488e94bf..6e1cfce6b9 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -14,7 +14,8 @@ name: Run unit tests # and code snippets (`check-docs-snippets`) jobs run checks only for the current LTS version of Node.js. # # For the Powertools for AWS main features (aka `packages/*`), instead we run linting and unit tests for all the supported -# versions of Node.js. +# versions of Node.js. The list of packages is derived from the root `package.json` workspaces by +# `.github/scripts/list_workspaces.js`, so adding a workspace there is all it takes to get it tested here. # # Since #2938, we are in the process of improving our test suite, so we are gradually extracting the tests for each package # from (`run-linting-check-and-unit-tests-on-utilities`) to their own job, so we can run them in parallel using the matrix @@ -23,7 +24,8 @@ name: Run unit tests # USAGE # # NOTE: meant to be called by ./.github/workflows/pr-run-linting-check-and-unit-tests.yml when a PR is opened or updated, -# or by ./.github/workflows/make-release.yml when a release is made. +# by ./.github/workflows/quality_check.yml when a PR enters the merge queue, or by ./.github/workflows/make-release.yml +# when a release is made. # on: @@ -32,7 +34,23 @@ on: permissions: {} jobs: + resolve-workspaces: + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + workspaces: ${{ steps.list.outputs.workspaces }} + steps: + - &checkout_code + name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: List non-private packages + id: list + run: | + workspaces=$(node .github/scripts/list_workspaces.js unit) + echo "workspaces=${workspaces}" >> "$GITHUB_OUTPUT" code-quality: + needs: resolve-workspaces permissions: contents: read runs-on: ubuntu-latest @@ -41,25 +59,10 @@ jobs: strategy: matrix: version: [22, 24] - workspace: [ - "packages/batch", - "packages/commons", - "packages/data-masking", - "packages/event-handler", - "packages/idempotency", - "packages/jmespath", - "packages/logger", - "packages/tracer", - "packages/parser", - "packages/parameters", - "packages/validation", - "packages/metrics", - "packages/kafka" - ] + workspace: ${{ fromJSON(needs.resolve-workspaces.outputs.workspaces) }} fail-fast: false steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - *checkout_code - name: Setup NodeJS uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -84,9 +87,7 @@ jobs: env: NODE_ENV: dev steps: - - &checkout_code - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - *checkout_code - &setup_node name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -131,11 +132,14 @@ jobs: # require, so that adding a Node.js version or a workspace to the `code-quality` matrix # doesn't silently change which checks are required. # + # `resolve-workspaces` is listed so that a failure to derive the matrix, which skips + # `code-quality` entirely, is reported here rather than passing as "nothing ran". + # # `if: always()` is required so this job still runs when a dependency fails - otherwise it # would be skipped, and a skipped required check counts as satisfied. For the same reason # `skipped` is treated as a failure below: none of the jobs above are conditional, so a skip # means something upstream went wrong. - needs: [code-quality, check-layer-publisher, check-docs-snippets, check-docs] + needs: [resolve-workspaces, code-quality, check-layer-publisher, check-docs-snippets, check-docs] if: always() runs-on: ubuntu-latest steps: diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 349c266402..3d7e06a408 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -46,13 +46,34 @@ jobs: - name: Deploy shared LMI capacity providers (all architectures) run: npm run lmi:deploy -w packages/testing + # Derives both e2e matrices from the workspaces that have suites under + # `tests/e2e`, so a package gets e2e coverage as soon as it has a suite. + resolve-workspaces: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + e2e: ${{ steps.list.outputs.e2e }} + lmi: ${{ steps.list.outputs.lmi }} + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: List workspaces with e2e suites + id: list + run: | + e2e=$(node .github/scripts/list_workspaces.js e2e) + lmi=$(node .github/scripts/list_workspaces.js e2e-lmi) + echo "e2e=${e2e}" >> "$GITHUB_OUTPUT" + echo "lmi=${lmi}" >> "$GITHUB_OUTPUT" + # Runs every package's e2e suites EXCEPT the LMI ones (each package's # `test:e2e` script excludes `tests/e2e/lmi.*`). Deliberately does NOT # depend on the LMI setup job: these suites don't use the capacity - # provider, so they fan out immediately and its EC2-backed provisioning - # time is amortized behind them. + # provider, so they fan out as soon as the matrix is resolved and its + # EC2-backed provisioning time is amortized behind them. run-e2e-tests-on-utils: runs-on: ubuntu-latest + needs: resolve-workspaces env: NODE_ENV: dev environment: e2e-tests @@ -62,19 +83,7 @@ jobs: strategy: max-parallel: 25 matrix: - package: - [ - packages/idempotency, - packages/logger, - packages/metrics, - packages/parameters, - packages/event-handler, - packages/tracer, - packages/batch, - packages/data-masking, - packages/signer, - layers, - ] + package: ${{ fromJSON(needs.resolve-workspaces.outputs.e2e) }} version: [22, 24] arch: [x86_64, arm64] fail-fast: false @@ -110,7 +119,7 @@ jobs: run-e2e-tests-lmi: runs-on: ubuntu-latest timeout-minutes: 60 - needs: setup-lmi-capacity-providers + needs: [setup-lmi-capacity-providers, resolve-workspaces] env: NODE_ENV: dev environment: e2e-tests @@ -119,7 +128,7 @@ jobs: contents: read strategy: matrix: - package: [packages/logger] + package: ${{ fromJSON(needs.resolve-workspaces.outputs.lmi) }} version: [22, 24] arch: [x86_64, arm64] fail-fast: false diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 9667806b6d..9ff3706f4a 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -143,12 +143,8 @@ Trusted publishers can only be configured for a package that **already exists** ### Wiring the package into CI -The PR that adds the package must also add it to: - -- the `workspaces` array in the root `package.json` — this is also what gets it into the Lambda layer, since the layer bundles every non-private `@aws-lambda-powertools/*` workspace; -- `.github/workflows/reusable-run-linting-check-and-unit-tests.yml`; -- `.github/workflows/quality_check.yml`; -- `.github/workflows/run-e2e-tests.yml`, if it has end-to-end tests. +The PR that adds the package must also add it to the `workspaces` array in the root `package.json`. That is all CI needs: the unit test and e2e workflows derive their package lists from it through `.github/scripts/list_workspaces.js`, the e2e workflow picking up any workspace with suites under `tests/e2e`. +The same array is what gets the package into the Lambda layer, since the layer bundles every non-private `@aws-lambda-powertools/*` workspace. Once that's done, the package ships like any other on the next `Make Release` run. diff --git a/packages/signer/package.json b/packages/signer/package.json index 029b0ebd11..ffa62bce6f 100644 --- a/packages/signer/package.json +++ b/packages/signer/package.json @@ -13,7 +13,7 @@ "test": "vitest --run tests/unit", "test:unit": "vitest --run tests/unit", "test:unit:coverage": "vitest --run tests/unit --coverage.enabled --coverage.thresholds.100 --coverage.include='src/**'", - "test:unit:types": "vitest --run tests/types --typecheck", + "test:unit:types": "echo 'Not Implemented'", "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e",