From 4677d3096f4e2a82d14aa159d388e2abb42f947c Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Fri, 21 Aug 2026 06:46:18 -0300 Subject: [PATCH 1/4] feat: add fluent-iterable-async-sema from fluent-iterable-monorepo Port @fluent-iterable/async-sema into the monorepo workspace. Includes source, tests, and config files adapted to the codibre pattern. Added to publish matrix in .github/workflows/publish.yaml. --- .github/workflows/publish.yaml | 1 + .../fluent-iterable-async-sema/.release-it.js | 3 + libs/fluent-iterable-async-sema/README.md | 86 +++++++++++++++++++ .../eslint.config.mjs | 4 + .../fluent-iterable-async-sema/jest.config.js | 3 + libs/fluent-iterable-async-sema/package.json | 57 ++++++++++++ libs/fluent-iterable-async-sema/src/index.ts | 3 + .../src/run-concurrently.declaration.ts | 26 ++++++ .../src/run-concurrently.ts | 23 +++++ .../src/sema-options.ts | 7 ++ .../test/jest-setup.ts | 10 +++ .../test/unit/run-concurrently.spec.ts | 85 ++++++++++++++++++ .../tsconfig.build.json | 9 ++ libs/fluent-iterable-async-sema/tsconfig.json | 11 +++ pnpm-lock.yaml | 21 +++++ 15 files changed, 349 insertions(+) create mode 100644 libs/fluent-iterable-async-sema/.release-it.js create mode 100644 libs/fluent-iterable-async-sema/README.md create mode 100644 libs/fluent-iterable-async-sema/eslint.config.mjs create mode 100644 libs/fluent-iterable-async-sema/jest.config.js create mode 100644 libs/fluent-iterable-async-sema/package.json create mode 100644 libs/fluent-iterable-async-sema/src/index.ts create mode 100644 libs/fluent-iterable-async-sema/src/run-concurrently.declaration.ts create mode 100644 libs/fluent-iterable-async-sema/src/run-concurrently.ts create mode 100644 libs/fluent-iterable-async-sema/src/sema-options.ts create mode 100644 libs/fluent-iterable-async-sema/test/jest-setup.ts create mode 100644 libs/fluent-iterable-async-sema/test/unit/run-concurrently.spec.ts create mode 100644 libs/fluent-iterable-async-sema/tsconfig.build.json create mode 100644 libs/fluent-iterable-async-sema/tsconfig.json diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 626fd2e..67bb68a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -23,6 +23,7 @@ jobs: - fluent-iterable - fluent-iterable-js-sdsl - fluent-iterable-rxjs + - fluent-iterable-async-sema steps: - name: Checkout repository diff --git a/libs/fluent-iterable-async-sema/.release-it.js b/libs/fluent-iterable-async-sema/.release-it.js new file mode 100644 index 0000000..e7eb0cc --- /dev/null +++ b/libs/fluent-iterable-async-sema/.release-it.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../.release-it.base.js'); + +module.exports = baseConfig; diff --git a/libs/fluent-iterable-async-sema/README.md b/libs/fluent-iterable-async-sema/README.md new file mode 100644 index 0000000..b22866e --- /dev/null +++ b/libs/fluent-iterable-async-sema/README.md @@ -0,0 +1,86 @@ +# @fluent-iterable/async-sema + +Lightweight integration between [async-sema](https://www.npmjs.com/package/async-sema) and [@codibre/fluent-iterable](https://www.npmjs.com/package/@codibre/fluent-iterable). + +This package adds a convenient `runConcurrently` resolving extension to both `fluent` and `fluentAsync` iterables so you can process items with a concurrency limit provided by `async-sema`. + +Features +- Use a familiar fluent-style API to run item handlers concurrently. +- Backed by `async-sema` so the semaphore is efficient and battle-tested. +- Works with both synchronous and asynchronous iterables. + +Installation + +```bash +npm i @codibre/fluent-iterable @fluent-iterable/async-sema +``` + +Quick Examples + +Important: this package registers the resolving extension when you import its `src/index` (packaged as `dist/index.js` in releases). Import it once before using the extension. + +Example for a synchronous iterable (fluent): + +```ts +import 'src/index'; // or import '@fluent-iterable/async-sema' after installing the package +import { fluent } from '@codibre/fluent-iterable'; + +const items = [1,2,3,4,5]; + +await fluent(items).runConcurrently({ maxConcurrency: 2 }, async (n) => { + // do work for item n + await doWork(n); +}); +``` + +Example for an async iterable (fluentAsync): + +```ts +import 'src/index'; +import { fluentAsync } from '@codibre/fluent-iterable'; + +async function* gen() { + for (let i = 1; i <= 5; i++) { + await delay(10); + yield i; + } +} + +await fluentAsync(gen()).runConcurrently({ maxConcurrency: 3 }, async (n) => { + await doWork(n); +}); +``` + +API + +runConcurrently(options, cb) + +- options: SemaOptions + - maxConcurrency: number (required) — maximum number of concurrent executions + - initFn?: () => unknown — forwarded to async-sema options + - pauseFn?: () => void + - resumeFn?: () => void + - capacity?: number + +- cb: (item) => void | Promise + +Behavior notes +- `runConcurrently` registers as a resolving extension. It acquires a semaphore permit for each item and schedules the callback with `setImmediate`. The function resolves once it finishes acquiring and scheduling callbacks for all items; callbacks themselves run next-tick. If you rely on callbacks finishing before proceeding, wait for their completion inside the callback or use your own signaling (tests in this repo show an example). + +Testing + +This package uses Jest + ts-jest. Tests live in `test/unit`. Run them from the package folder: + +```bash +cd libs/fluent-iterable-async-sema +pnpm test +``` + +Contributing + +- Keep tests fast: use small delays (tens of milliseconds) when simulating async work. +- When adding features that change how `runConcurrently` resolves, update tests accordingly: current implementation schedules callbacks with `setImmediate`, so tests must wait for completion explicitly if they assert on callback side effects. + +License + +ISC diff --git a/libs/fluent-iterable-async-sema/eslint.config.mjs b/libs/fluent-iterable-async-sema/eslint.config.mjs new file mode 100644 index 0000000..6246a6d --- /dev/null +++ b/libs/fluent-iterable-async-sema/eslint.config.mjs @@ -0,0 +1,4 @@ +import rules from '../../eslint.config.mjs'; + +/** @type {import('eslint').ESLint} **/ +export default rules; diff --git a/libs/fluent-iterable-async-sema/jest.config.js b/libs/fluent-iterable-async-sema/jest.config.js new file mode 100644 index 0000000..6ae1345 --- /dev/null +++ b/libs/fluent-iterable-async-sema/jest.config.js @@ -0,0 +1,3 @@ +const jest = require('../../jest.config'); + +module.exports = jest; diff --git a/libs/fluent-iterable-async-sema/package.json b/libs/fluent-iterable-async-sema/package.json new file mode 100644 index 0000000..0a6004e --- /dev/null +++ b/libs/fluent-iterable-async-sema/package.json @@ -0,0 +1,57 @@ +{ + "name": "@fluent-iterable/async-sema", + "description": "async-sema integration with @codibre/fluent-iterable", + "version": "0.1.1", + "private": false, + "author": { + "name": "Farenheith" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "jest test/unit", + "lint": "pnpm run lint:format && pnpm run lint:style", + "lint:fix": "pnpm run lint:format:fix && pnpm run lint:style:fix", + "lint:staged": "lint-staged -c ../../.lintstagedrc", + "lint:format": "prettier --check '{src,test}/**/*.ts'", + "lint:format:fix": "prettier --write '{src,test}/**/*.ts'", + "lint:style": "eslint '**/*.ts'", + "lint:style:fix": "eslint '**/*.ts' --fix" + }, + "engines": { + "node": ">=10" + }, + "keywords": [ + "fluent-iterable", + "fluent interface", + "Iterable", + "AsyncIterable", + "Stream" + ], + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/codibre/fluent-iterable" + }, + "homepage": "https://github.com/codibre/fluent-iterable#readme", + "bugs": { + "url": "https://github.com/codibre/fluent-iterable/issues" + }, + "dependencies": { + "@codibre/fluent-iterable": "workspace:^" + }, + "peerDependencies": { + "async-sema": "^3.1.1" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "del-cli": "^5.1.0", + "jest-extended": "^4.0.2" + } +} diff --git a/libs/fluent-iterable-async-sema/src/index.ts b/libs/fluent-iterable-async-sema/src/index.ts new file mode 100644 index 0000000..c9461ba --- /dev/null +++ b/libs/fluent-iterable-async-sema/src/index.ts @@ -0,0 +1,3 @@ +export * from './run-concurrently'; +export * from './run-concurrently.declaration'; +export * from './sema-options'; diff --git a/libs/fluent-iterable-async-sema/src/run-concurrently.declaration.ts b/libs/fluent-iterable-async-sema/src/run-concurrently.declaration.ts new file mode 100644 index 0000000..a0d61ac --- /dev/null +++ b/libs/fluent-iterable-async-sema/src/run-concurrently.declaration.ts @@ -0,0 +1,26 @@ +import { + Action, + AsyncAction, + extend, + extendAsync, +} from '@codibre/fluent-iterable'; +import { SemaOptions } from './sema-options'; +import { runConcurrently } from './run-concurrently'; + +declare module '@codibre/fluent-iterable' { + interface FluentIterable { + runConcurrently( + options: SemaOptions, + cb: Action | AsyncAction, + ): Promise; + } + interface FluentAsyncIterable { + runConcurrently( + options: SemaOptions, + cb: Action | AsyncAction, + ): Promise; + } +} + +extend.useResolving(runConcurrently.name, runConcurrently); +extendAsync.useResolving(runConcurrently.name, runConcurrently); diff --git a/libs/fluent-iterable-async-sema/src/run-concurrently.ts b/libs/fluent-iterable-async-sema/src/run-concurrently.ts new file mode 100644 index 0000000..b337bf6 --- /dev/null +++ b/libs/fluent-iterable-async-sema/src/run-concurrently.ts @@ -0,0 +1,23 @@ +import { Action, AsyncAction } from '@codibre/fluent-iterable'; +import { SemaOptions } from './sema-options'; +import { Sema } from 'async-sema'; + +export async function runConcurrently( + this: Iterable | AsyncIterable, + options: SemaOptions, + cb: Action | AsyncAction, +) { + const sema = new Sema(options.maxConcurrency, options); + + for await (const item of this) { + const release = await sema.acquire(); + setImmediate(async () => { + try { + await cb(item); + } finally { + sema.release(release); + } + }); + } + await sema.drain(); +} diff --git a/libs/fluent-iterable-async-sema/src/sema-options.ts b/libs/fluent-iterable-async-sema/src/sema-options.ts new file mode 100644 index 0000000..3596543 --- /dev/null +++ b/libs/fluent-iterable-async-sema/src/sema-options.ts @@ -0,0 +1,7 @@ +export interface SemaOptions { + maxConcurrency: number; + initFn?: () => unknown; + pauseFn?: () => void; + resumeFn?: () => void; + capacity?: number; +} diff --git a/libs/fluent-iterable-async-sema/test/jest-setup.ts b/libs/fluent-iterable-async-sema/test/jest-setup.ts new file mode 100644 index 0000000..20b9da2 --- /dev/null +++ b/libs/fluent-iterable-async-sema/test/jest-setup.ts @@ -0,0 +1,10 @@ +import 'jest-callslike'; +import 'jest-extended'; + +const matchers = require('jest-extended'); +expect.extend(matchers); + +afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); +}); diff --git a/libs/fluent-iterable-async-sema/test/unit/run-concurrently.spec.ts b/libs/fluent-iterable-async-sema/test/unit/run-concurrently.spec.ts new file mode 100644 index 0000000..6ab0924 --- /dev/null +++ b/libs/fluent-iterable-async-sema/test/unit/run-concurrently.spec.ts @@ -0,0 +1,85 @@ +import '../../src'; // load the plugin + +import { fluent, fluentAsync } from '@codibre/fluent-iterable'; + +const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +describe('runConcurrently (async-sema integration)', () => { + it('respects maxConcurrency for a sync iterable (fluent)', async () => { + const items = [1, 2, 3, 4, 5]; + const maxConcurrency = 2; + + let running = 0; + let maxSeen = 0; + const completed: number[] = []; + + // promise that resolves when all tasks finished + let resolveAll: () => void; + const allDone = new Promise((r) => (resolveAll = r)); + + await fluent(items).runConcurrently( + { maxConcurrency }, + async (n: number) => { + running++; + maxSeen = Math.max(maxSeen, running); + + // simulate variable work: longer for larger numbers + await wait(n * 20); + + completed.push(n); + + running--; + if (completed.length === items.length) resolveAll(); + }, + ); + + // runConcurrently may return before every callback finishes (callbacks run via setImmediate), + // so wait for all to finish as signaled above. + await allDone; + + expect(maxSeen).toBeLessThanOrEqual(maxConcurrency); + // all items should have been processed + expect(completed).toIncludeSameMembers(items); + }); + + it('respects maxConcurrency for an async iterable (fluentAsync)', async () => { + async function* gen() { + for (const n of [1, 2, 3, 4, 5]) { + // small delay between yields to better exercise interleaving + await wait(10); + yield n; + } + } + + const items = [1, 2, 3, 4, 5]; + const maxConcurrency = 3; + + let running = 0; + let maxSeen = 0; + const completed: number[] = []; + + let resolveAll: () => void; + const allDone = new Promise((r) => (resolveAll = r)); + + await fluentAsync(gen()).runConcurrently( + { maxConcurrency }, + async (n: number) => { + running++; + maxSeen = Math.max(maxSeen, running); + + // variable work + await wait(n * 15); + + completed.push(n); + + running--; + if (completed.length === items.length) resolveAll(); + }, + ); + + await allDone; + + expect(maxSeen).toBeLessThanOrEqual(maxConcurrency); + expect(completed).toIncludeSameMembers(items); + }); +}); diff --git a/libs/fluent-iterable-async-sema/tsconfig.build.json b/libs/fluent-iterable-async-sema/tsconfig.build.json new file mode 100644 index 0000000..f3acacf --- /dev/null +++ b/libs/fluent-iterable-async-sema/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": [ + "./src" + ] +} diff --git a/libs/fluent-iterable-async-sema/tsconfig.json b/libs/fluent-iterable-async-sema/tsconfig.json new file mode 100644 index 0000000..8588024 --- /dev/null +++ b/libs/fluent-iterable-async-sema/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "src", + "test" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e073a7d..3082704 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,22 @@ importers: specifier: ^4.2.10 version: 4.12.0(typedoc@0.26.11(typescript@5.6.2)) + libs/fluent-iterable-async-sema: + dependencies: + '@codibre/fluent-iterable': + specifier: workspace:^ + version: link:../fluent-iterable + async-sema: + specifier: ^3.1.1 + version: 3.1.1 + devDependencies: + del-cli: + specifier: ^5.1.0 + version: 5.1.0 + jest-extended: + specifier: ^4.0.2 + version: 4.0.2(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.6.2))) + libs/fluent-iterable-js-sdsl: dependencies: '@codibre/fluent-iterable': @@ -1142,6 +1158,9 @@ packages: ast-v8-to-istanbul@1.0.5: resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4767,6 +4786,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async-sema@3.1.1: {} + babel-jest@29.7.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 From 44534e76ec1774d69dd86495fff6d64dc8404656 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Fri, 21 Aug 2026 06:58:12 -0300 Subject: [PATCH 2/4] chore: removing unneeded benchmarks Only lts matters --- .github/workflows/benchmark.yaml | 12 ++---------- package.json | 3 ++- turbo.json | 6 +++++- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 6a8792e..1842ed0 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -10,9 +10,6 @@ on: jobs: benchmark: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [26.x, 24.x, 22.x] steps: - name: Checkout repository uses: actions/checkout@v7 @@ -21,13 +18,8 @@ jobs: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v7 with: - node-version: ${{ matrix.node-version }} cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build - run: pnpm build - - name: Benchmark augmentative-iterable - run: pnpm --filter augmentative-iterable test:benchmark - - name: Benchmark fluent-iterable - run: pnpm --filter @codibre/fluent-iterable test:benchmark + - name: Benchmark + run: pnpm test:benchmark diff --git a/package.json b/package.json index af695b6..bafce8a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "prettier --write '**/*.{ts,json,md}'", "test:cov": "turbo test:coverage", "prepare": "husky", - "lint:staged": "turbo lint:staged --concurrency=1" + "lint:staged": "turbo lint:staged --concurrency=1", + "test:benchmark": "turbo test:benchmark --concurrency=1" }, "keywords": [ "monorepo", diff --git a/turbo.json b/turbo.json index 0f177a1..0363308 100644 --- a/turbo.json +++ b/turbo.json @@ -14,6 +14,10 @@ "lint:format": {}, "lint:format:fix": {}, "lint:style": {}, - "lint:style:fix": {} + "lint:style:fix": {}, + "test:benchmark": { + "dependsOn": ["build"], + "cache": false + } } } From bb89657ad99bb800970b76ac8a1420c1b7061841 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Fri, 21 Aug 2026 07:26:41 -0300 Subject: [PATCH 3/4] perf: sequential publish in single job with OIDC token caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace matrix strategy with a single job running packages sequentially - Discover non-private packages dynamically via pnpm workspace scan - Obtain OIDC token once and reuse within 4-minute window (token TTL ~5 min) - Eliminates per-job runner overhead (~30s × N jobs → one setup) --- .github/workflows/publish.yaml | 90 ++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 67bb68a..008863b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -14,17 +14,6 @@ jobs: publish: runs-on: ubuntu-latest - strategy: - fail-fast: false - max-parallel: 1 - matrix: - library: - - augmentative-iterable - - fluent-iterable - - fluent-iterable-js-sdsl - - fluent-iterable-rxjs - - fluent-iterable-async-sema - steps: - name: Checkout repository uses: actions/checkout@v7 @@ -59,26 +48,67 @@ jobs: env: HUSKY: 0 - - name: Get OIDC Token - id: oidc - run: | - OIDC_TOKEN=$(curl -sL "${ACTIONS_ID_TOKEN_REQUEST_URL}?audience=https://registry.npmjs.org" \ - -H "Authorization: Bearer ${ACTIONS_ID_TOKEN}" | jq -r '.value') - echo "oidc-token=${OIDC_TOKEN}" >> $GITHUB_OUTPUT + - name: Install release-it + run: npm install -g release-it@21.0.2 @release-it/conventional-changelog@12.0.0 - - name: Configure npm authentication + - name: Publish libraries sequentially run: | - echo "//registry.npmjs.org/:_authToken=${{ steps.oidc.outputs.oidc-token }}" > ~/.npmrc - echo "registry=https://registry.npmjs.org/" >> ~/.npmrc - echo "always-auth=true" >> ~/.npmrc - env: - OIDC_TOKEN: ${{ steps.oidc.outputs.oidc-token }} + set -euo pipefail - - name: Install release-it - run: npm install -g release-it@21.0.2 @release-it/conventional-changelog@12.0.0 + OIDC_TOKEN="" + TOKEN_OBTAINED_AT=0 - - name: Release ${{ matrix.library }} - working-directory: ./libs/${{ matrix.library }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: release-it --ci + # Get list of non-private packages in libs/ + PACKAGES=$(for lib in libs/*/; do + if [ -f "${lib}package.json" ]; then + name=$(node -p "require('./${lib}package.json').name") + private=$(node -p "try{require('./${lib}package.json').private}catch(e){'undefined'}") + if [ "$private" != "true" ] && [ -n "$name" ]; then + echo "${name}|${lib}" + fi + fi + done) + + echo "Packages to publish:" + echo "$PACKAGES" | while IFS='|' read -r name path; do echo " - $name"; done + + # Get OIDC token helper (valid ~5 min from GitHub) + get_oidc_token() { + curl -sL "${ACTIONS_ID_TOKEN_REQUEST_URL}?audience=https://registry.npmjs.org" \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN}" | jq -r '.value' + } + + configure_npmrc() { + local token="$1" + echo "//registry.npmjs.org/:_authToken=${token}" > ~/.npmrc + echo "registry=https://registry.npmjs.org/" >> ~/.npmrc + echo "always-auth=true" >> ~/.npmrc + } + + NOW=$(date +%s) + # Token TTL is ~5 min; refresh if older than 4 min + TOKEN_TTL=240 + + for entry in $PACKAGES; do + IFS='|' read -r pkg_name lib_path <<< "$entry" + pkg_dir=$(realpath ".${lib_path}") + echo "" + echo ">>> Publishing ${pkg_name} from ${pkg_dir}" + + # Refresh OIDC token if missing or older than 4 minutes + if [ $((NOW - TOKEN_OBTAINED_AT)) -gt $TOKEN_TTL ] || [ -z "$OIDC_TOKEN" ]; then + echo " Obtaining fresh OIDC token..." + OIDC_TOKEN=$(get_oidc_token) + TOKEN_OBTAINED_AT=$(date +%s) + fi + + configure_npmrc "$OIDC_TOKEN" + + ( + cd "$pkg_dir" + release-it --ci + ) + done + + echo "" + echo "All libraries published successfully." From b605852dbc7c83c866b2bea9ed1ff9b37c0fc838 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Fri, 21 Aug 2026 07:47:40 -0300 Subject: [PATCH 4/4] chore: accurate publish summary message in workflow Track published vs skipped libs separately and report correctly. --- .github/workflows/publish.yaml | 35 +++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 008863b..c11c1d2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -57,6 +57,8 @@ jobs: OIDC_TOKEN="" TOKEN_OBTAINED_AT=0 + PUBLISHED=() + SKIPPED=() # Get list of non-private packages in libs/ PACKAGES=$(for lib in libs/*/; do @@ -69,7 +71,7 @@ jobs: fi done) - echo "Packages to publish:" + echo "Packages to check:" echo "$PACKAGES" | while IFS='|' read -r name path; do echo " - $name"; done # Get OIDC token helper (valid ~5 min from GitHub) @@ -93,7 +95,7 @@ jobs: IFS='|' read -r pkg_name lib_path <<< "$entry" pkg_dir=$(realpath ".${lib_path}") echo "" - echo ">>> Publishing ${pkg_name} from ${pkg_dir}" + echo ">>> Checking ${pkg_name} from ${pkg_dir}" # Refresh OIDC token if missing or older than 4 minutes if [ $((NOW - TOKEN_OBTAINED_AT)) -gt $TOKEN_TTL ] || [ -z "$OIDC_TOKEN" ]; then @@ -104,11 +106,30 @@ jobs: configure_npmrc "$OIDC_TOKEN" - ( - cd "$pkg_dir" - release-it --ci - ) + # Capture release-it output to detect if it actually published + OUTPUT=$(cd "$pkg_dir" && release-it --ci 2>&1 || true) + echo "$OUTPUT" + + # Check if the output contains a version bump (not just SKIP) + if echo "$OUTPUT" | grep -qE "release [0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+\.[0-9]+\.\.\.[0-9]+\.[0-9]+\.[0-9]+" && ! echo "$OUTPUT" | grep -q "No commits found"; then + PUBLISHED+=("$pkg_name") + else + SKIPPED+=("$pkg_name") + fi done echo "" - echo "All libraries published successfully." + echo "=== Summary ===" + if [ ${#PUBLISHED[@]} -gt 0 ]; then + echo "Published:" + for pkg in "${PUBLISHED[@]}"; do echo " ✓ $pkg"; done + fi + if [ ${#SKIPPED[@]} -gt 0 ]; then + echo "Skipped (no new commits):" + for pkg in "${SKIPPED[@]}"; do echo " ⏭️ $pkg"; done + fi + if [ ${#PUBLISHED[@]} -eq 0 ] && [ ${#SKIPPED[@]} -gt 0 ]; then + echo "No libraries needed publishing this run." + elif [ ${#PUBLISHED[@]} -gt 0 ]; then + echo "Successfully published ${#PUBLISHED[@]} library(ies)." + fi