From 1a37f27184eea377d83d4dd371f4fef13c9b9cdb Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sun, 12 Jul 2026 16:09:46 +0200 Subject: [PATCH] chore: publish components from split repositories --- .github/workflows/cd.yml | 52 ++++---------- .github/workflows/publish-component.yml | 58 +++++++++++++++ .github/workflows/split-components.yml | 28 ++++++-- package-lock.json | 5 ++ scripts/release/generate-release-plan.ts | 54 -------------- scripts/release/package-selection.test.ts | 67 ------------------ scripts/release/package-selection.ts | 59 ---------------- scripts/release/publish-release.test.ts | 40 ----------- scripts/release/publish-release.ts | 67 ------------------ scripts/release/release-plan.test.ts | 61 ---------------- scripts/release/release-plan.ts | 46 ------------ scripts/release/release-staging.test.ts | 56 --------------- scripts/release/release-staging.ts | 70 ------------------- scripts/release/stage-release.ts | 58 --------------- scripts/tsconfig.json | 10 --- .../contracts/.github/workflows/publish.yml | 25 +++++++ src/packages/contracts/package.json | 8 ++- templates/component-release/tsconfig.json | 27 +++++++ 18 files changed, 156 insertions(+), 635 deletions(-) create mode 100644 .github/workflows/publish-component.yml delete mode 100644 scripts/release/generate-release-plan.ts delete mode 100644 scripts/release/package-selection.test.ts delete mode 100644 scripts/release/package-selection.ts delete mode 100644 scripts/release/publish-release.test.ts delete mode 100644 scripts/release/publish-release.ts delete mode 100644 scripts/release/release-plan.test.ts delete mode 100644 scripts/release/release-plan.ts delete mode 100644 scripts/release/release-staging.test.ts delete mode 100644 scripts/release/release-staging.ts delete mode 100644 scripts/release/stage-release.ts delete mode 100644 scripts/tsconfig.json create mode 100644 src/packages/contracts/.github/workflows/publish.yml create mode 100644 templates/component-release/tsconfig.json diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 276c391..819bdbd 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -9,14 +9,6 @@ on: description: Canonical release tag to publish required: true type: string - release_mode: - description: Select changed packages or every package - default: changed - required: true - type: choice - options: - - changed - - all jobs: cd: @@ -39,44 +31,19 @@ jobs: cache: 'npm' registry-url: 'https://registry.npmjs.org' - - name: Configure release + - name: Select release tag id: release env: EVENT_NAME: ${{ github.event_name }} - RELEASE_BODY: ${{ github.event.release.body }} RELEASE_TAG: ${{ github.event.release.tag_name }} - INPUT_RELEASE_MODE: ${{ inputs.release_mode }} INPUT_RELEASE_TAG: ${{ inputs.release_tag }} run: | if [ "$EVENT_NAME" = 'release' ]; then echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" - if printf '%s\n' "$RELEASE_BODY" | grep -Eq '(^|[[:space:]])release-mode:[[:space:]]*all([[:space:]]|$)'; then - echo 'mode=all' >> "$GITHUB_OUTPUT" - else - echo 'mode=changed' >> "$GITHUB_OUTPUT" - fi else echo "tag=$INPUT_RELEASE_TAG" >> "$GITHUB_OUTPUT" - echo "mode=$INPUT_RELEASE_MODE" >> "$GITHUB_OUTPUT" fi - - name: Generate release plan - id: release_plan - env: - RELEASE_MODE: ${{ steps.release.outputs.mode }} - RELEASE_PLAN_PATH: ${{ runner.temp }}/release-plan.json - run: | - node scripts/release/generate-release-plan.ts "${{ steps.release.outputs.tag }}" > "$RELEASE_PLAN_PATH" - package_count=$(node -e 'const { packageNames } = JSON.parse(require("node:fs").readFileSync(process.env.RELEASE_PLAN_PATH)); process.stdout.write(String(packageNames.length));') - echo "package_count=$package_count" >> "$GITHUB_OUTPUT" - - - name: Stage release manifests - env: - RELEASE_PLAN_PATH: ${{ runner.temp }}/release-plan.json - run: | - node scripts/release/stage-release.ts "${{ steps.release.outputs.tag }}" "$RELEASE_PLAN_PATH" - npm install - - name: Check code run: npm run lint @@ -95,10 +62,17 @@ jobs: - name: Check packages run: npm run publint - - name: Publish packages - if: steps.release_plan.outputs.package_count != '0' + - name: Publish framework env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - RELEASE_PLAN_PATH: ${{ runner.temp }}/release-plan.json - run: | - node scripts/release/publish-release.ts "${{ steps.release.outputs.tag }}" "$RELEASE_PLAN_PATH" + run: npm publish + + split_components: + needs: cd + uses: ./.github/workflows/split-components.yml + with: + source_ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }} + source_branch: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }} + source_type: tag + sync: true + secrets: inherit diff --git a/.github/workflows/publish-component.yml b/.github/workflows/publish-component.yml new file mode 100644 index 0000000..aaff519 --- /dev/null +++ b/.github/workflows/publish-component.yml @@ -0,0 +1,58 @@ +name: Publish component + +on: + workflow_call: + inputs: + framework_ref: + description: Framework branch containing the release templates + required: true + type: string + dry_run: + description: Validate the package without publishing it + required: false + default: false + type: boolean + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout component + uses: actions/checkout@v4 + + - name: Checkout release templates + uses: actions/checkout@v4 + with: + repository: koala-ts/framework + ref: ${{ inputs.framework_ref }} + path: framework + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm install --package-lock=false + + - name: Apply release TypeScript configuration + run: cp framework/templates/component-release/tsconfig.json tsconfig.json + + - name: Check types + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Check package + run: npm run publint + + - name: Publish package + if: ${{ !inputs.dry_run }} + run: npm publish diff --git a/.github/workflows/split-components.yml b/.github/workflows/split-components.yml index f223831..576c343 100644 --- a/.github/workflows/split-components.yml +++ b/.github/workflows/split-components.yml @@ -4,8 +4,6 @@ on: push: branches: - 2.x - tags: - - '*' workflow_dispatch: inputs: source_branch: @@ -18,6 +16,24 @@ on: required: true default: false type: boolean + workflow_call: + inputs: + source_ref: + description: Source Git reference to split + required: true + type: string + source_branch: + description: Destination branch or tag name + required: true + type: string + source_type: + description: Type of destination Git reference + required: true + type: string + sync: + description: Push the split reference to component repositories + required: true + type: boolean jobs: split: @@ -31,7 +47,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.source_branch || github.ref }} + ref: ${{ github.event_name == 'workflow_call' && inputs.source_ref || github.event_name == 'workflow_dispatch' && inputs.source_branch || github.ref }} - name: Install splitsh-lite run: | @@ -49,7 +65,7 @@ jobs: - name: Split components env: SPLITTER_TOKEN: ${{ steps.app-token.outputs.token }} - SOURCE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.source_branch || github.ref_name }} - SOURCE_TYPE: ${{ github.event_name == 'workflow_dispatch' && 'branch' || github.ref_type }} - SYNC: ${{ github.event_name != 'workflow_dispatch' || inputs.sync }} + SOURCE_BRANCH: ${{ github.event_name == 'workflow_call' && inputs.source_branch || github.event_name == 'workflow_dispatch' && inputs.source_branch || github.ref_name }} + SOURCE_TYPE: ${{ github.event_name == 'workflow_call' && inputs.source_type || github.event_name == 'workflow_dispatch' && 'branch' || github.ref_type }} + SYNC: ${{ github.event_name == 'workflow_call' && inputs.sync || github.event_name != 'workflow_dispatch' || inputs.sync }} run: bash scripts/split/split-components.sh diff --git a/package-lock.json b/package-lock.json index 15890c2..37bf5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4743,6 +4743,11 @@ "name": "@koala-ts/contracts", "version": "2.0.0", "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^24.13.3", + "publint": "^0.3.21", + "typescript": "^7.0.2" + }, "engines": { "node": ">=24", "npm": ">=11" diff --git a/scripts/release/generate-release-plan.ts b/scripts/release/generate-release-plan.ts deleted file mode 100644 index 06ea516..0000000 --- a/scripts/release/generate-release-plan.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { readdirSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; - -import { createReleasePlan, findPreviousTag, getChangedFiles, getUnpublishedPackageNames } from './release-plan.ts'; - -const releaseTag = process.argv[2]; - -if (!releaseTag) { - throw new Error('A release tag is required.'); -} - -const runGit = arguments_ => execFileSync('git', arguments_, { encoding: 'utf8' }); -const previousTag = findPreviousTag({ releaseTag, runGit }); -const changedFiles = getChangedFiles({ previousTag, releaseTag, runGit }); -const packageDefinitions = [readPackageDefinition('.'), ...readWorkspacePackageDefinitions()]; -const releaseMode = process.env.RELEASE_MODE === 'all' ? 'all' : undefined; -const unpublishedPackageNames = getUnpublishedPackageNames({ getPublishedVersion, packages: packageDefinitions }); -const releasePlan = createReleasePlan({ - changedFiles, - packages: packageDefinitions, - releaseMode, - unpublishedPackageNames, -}); - -process.stdout.write(`${JSON.stringify({ previousTag, ...releasePlan })}\n`); - -function readWorkspacePackageDefinitions() { - return readdirSync('src/packages', { withFileTypes: true }) - .filter(directoryEntry => directoryEntry.isDirectory()) - .map(directoryEntry => readPackageDefinition(join('src/packages', directoryEntry.name))); -} - -function readPackageDefinition(path) { - const manifestPath = path === '.' ? 'package.json' : join(path, 'package.json'); - const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); - const dependencies = Object.keys(manifest.dependencies ?? {}).filter(dependency => - dependency.startsWith('@koala-ts/'), - ); - - return { name: manifest.name, path, dependencies }; -} - -function getPublishedVersion(packageName) { - try { - return execFileSync('npm', ['view', packageName, 'version'], { encoding: 'utf8' }).trim(); - } catch (error) { - if (error.status === 1 && error.stderr.includes('E404')) { - return undefined; - } - - throw error; - } -} diff --git a/scripts/release/package-selection.test.ts b/scripts/release/package-selection.test.ts deleted file mode 100644 index 12aa527..0000000 --- a/scripts/release/package-selection.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { type Package, selectChangedPackages, sortPackagesForPublishing } from './package-selection.ts'; - -describe('release package selection', () => { - it('includes the component whose source changed', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.' }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - - const selectedPackages = selectChangedPackages({ changedFiles: ['src/packages/contracts/index.ts'], packages }); - - expect(selectedPackages).toEqual(['@koala-ts/contracts']); - }); - - it('includes the framework when framework source changed', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.' }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - - const selectedPackages = selectChangedPackages({ - changedFiles: ['src/application/create-application.ts'], - packages, - }); - - expect(selectedPackages).toEqual(['@koala-ts/framework']); - }); - - it('includes every package in a full release', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.' }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - - const selectedPackages = selectChangedPackages({ changedFiles: [], packages, releaseMode: 'all' }); - - expect(selectedPackages).toEqual(['@koala-ts/framework', '@koala-ts/contracts']); - }); - - it('publishes a dependency before its dependent', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.', dependencies: ['@koala-ts/contracts'] }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - - const orderedPackages = sortPackagesForPublishing({ - packages, - selectedPackageNames: ['@koala-ts/framework', '@koala-ts/contracts'], - }); - - expect(orderedPackages).toEqual(['@koala-ts/contracts', '@koala-ts/framework']); - }); - - it('rejects packages with a circular dependency', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.', dependencies: ['@koala-ts/contracts'] }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts', dependencies: ['@koala-ts/framework'] }, - ]; - - const sortPackages = () => - sortPackagesForPublishing({ packages, selectedPackageNames: packages.map(({ name }) => name) }); - - expect(sortPackages).toThrow('Selected packages contain a dependency cycle.'); - }); -}); diff --git a/scripts/release/package-selection.ts b/scripts/release/package-selection.ts deleted file mode 100644 index 8600754..0000000 --- a/scripts/release/package-selection.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @typedef {object} Package - * @property {string} name - * @property {string} path - * @property {string[]} [dependencies] - */ - -/** - * @param {{ changedFiles: string[]; packages: Package[]; releaseMode?: 'all' }} options - * @returns {string[]} - */ -export const selectChangedPackages = ({ changedFiles, packages, releaseMode }) => { - if (releaseMode === 'all') { - return packages.map(({ name }) => name); - } - - return packages - .filter(packageDefinition => changedFiles.some(changedFile => belongsToPackage({ changedFile, packageDefinition }))) - .map(({ name }) => name); -}; - -/** - * @param {{ changedFile: string; packageDefinition: Package }} options - * @returns {boolean} - */ -const belongsToPackage = ({ changedFile, packageDefinition }) => { - if (packageDefinition.path === '.') { - return !changedFile.startsWith('src/packages/'); - } - - return changedFile === packageDefinition.path || changedFile.startsWith(`${packageDefinition.path}/`); -}; - -/** - * @param {{ packages: Package[]; selectedPackageNames: string[] }} options - * @returns {string[]} - */ -export const sortPackagesForPublishing = ({ packages, selectedPackageNames }) => { - const selectedPackages = packages.filter(({ name }) => selectedPackageNames.includes(name)); - const remainingPackages = new Map( - selectedPackages.map(packageDefinition => [packageDefinition.name, packageDefinition]), - ); - const orderedPackages = []; - - while (remainingPackages.size > 0) { - const publishablePackage = [...remainingPackages.values()].find(({ dependencies = [] }) => - dependencies.every(dependency => !remainingPackages.has(dependency)), - ); - - if (!publishablePackage) { - throw new Error('Selected packages contain a dependency cycle.'); - } - - orderedPackages.push(publishablePackage.name); - remainingPackages.delete(publishablePackage.name); - } - - return orderedPackages; -}; diff --git a/scripts/release/publish-release.test.ts b/scripts/release/publish-release.test.ts deleted file mode 100644 index 4d4ddc9..0000000 --- a/scripts/release/publish-release.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { selectPackagesToPublish } from './publish-release.ts'; - -describe('release publishing', () => { - it('selects the root package and every selected component that is not published at the release version', () => { - const packageNames = ['@koala-ts/contracts', '@koala-ts/framework']; - const getPublishedVersion = () => undefined; - - const packageNamesToPublish = selectPackagesToPublish({ - getPublishedVersion, - packageNames, - releaseTag: '2.28.7', - }); - - expect(packageNamesToPublish).toEqual(['@koala-ts/contracts', '@koala-ts/framework']); - }); - - it('skips a package already published at the release version', () => { - const packageNames = ['@koala-ts/contracts', '@koala-ts/framework']; - const getPublishedVersion = (packageName: string) => (packageName === '@koala-ts/contracts' ? '2.28.7' : undefined); - - const packageNamesToPublish = selectPackagesToPublish({ - getPublishedVersion, - packageNames, - releaseTag: '2.28.7', - }); - - expect(packageNamesToPublish).toEqual(['@koala-ts/framework']); - }); - - it('rejects a package with an unexpected published version', () => { - const packageNames = ['@koala-ts/contracts']; - const getPublishedVersion = () => '2.28.6'; - - const selectPackages = () => selectPackagesToPublish({ getPublishedVersion, packageNames, releaseTag: '2.28.7' }); - - expect(selectPackages).toThrow('@koala-ts/contracts is already published as 2.28.6.'); - }); -}); diff --git a/scripts/release/publish-release.ts b/scripts/release/publish-release.ts deleted file mode 100644 index 909e2de..0000000 --- a/scripts/release/publish-release.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; - -const [releaseTag, releasePlanPath] = process.argv.slice(2); - -if (releaseTag && releasePlanPath) { - const { packageNames } = JSON.parse(readFileSync(releasePlanPath, 'utf8')); - const packageNamesToPublish = selectPackagesToPublish({ - getPublishedVersion, - packageNames, - releaseTag, - }); - - for (const packageName of packageNamesToPublish) { - publishPackage(packageName); - } - - const skippedPackageNames = packageNames.filter(packageName => !packageNamesToPublish.includes(packageName)); - process.stdout.write(`Published packages: ${packageNamesToPublish.join(', ') || 'none'}\n`); - process.stdout.write(`Skipped packages: ${skippedPackageNames.join(', ') || 'none'}\n`); -} - -/** - * @param {{ - * packageNames: string[]; - * releaseTag: string; - * getPublishedVersion: (packageName: string) => string | undefined; - * }} options - * @returns {string[]} - */ -export function selectPackagesToPublish({ getPublishedVersion, packageNames, releaseTag }) { - return packageNames.filter(packageName => { - const publishedVersion = getPublishedVersion(packageName); - - if (!publishedVersion) { - return true; - } - - if (publishedVersion === releaseTag) { - return false; - } - - throw new Error(`${packageName} is already published as ${publishedVersion}.`); - }); -} - -function getPublishedVersion(packageName) { - try { - return execFileSync('npm', ['view', `${packageName}@${releaseTag}`, 'version'], { encoding: 'utf8' }).trim(); - } catch (error) { - if (error.status === 1 && error.stderr.includes('E404')) { - return undefined; - } - - throw error; - } -} - -function publishPackage(packageName) { - const arguments_ = ['publish', '--provenance', '--access', 'public']; - - if (packageName !== '@koala-ts/framework') { - arguments_.push(`--workspace=${packageName}`); - } - - execFileSync('npm', arguments_, { stdio: 'inherit' }); -} diff --git a/scripts/release/release-plan.test.ts b/scripts/release/release-plan.test.ts deleted file mode 100644 index dcfd989..0000000 --- a/scripts/release/release-plan.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - createReleasePlan, - findPreviousTag, - getChangedFiles, - getUnpublishedPackageNames, - type Package, -} from './release-plan.ts'; - -describe('release plan', () => { - it('uses the tag before the release tag as the comparison point', () => { - const calls: string[][] = []; - const runGit = (arguments_: string[]) => { - calls.push(arguments_); - return '2.18.1\n'; - }; - - const previousTag = findPreviousTag({ releaseTag: '2.19.0', runGit }); - - expect(previousTag).toBe('2.18.1'); - expect(calls).toEqual([['describe', '--tags', '--abbrev=0', '2.19.0^']]); - }); - - it('lists the files changed since the previous tag', () => { - const runGit = () => 'src/packages/contracts/index.ts\nsrc/index.ts\n'; - - const changedFiles = getChangedFiles({ previousTag: '2.18.1', releaseTag: '2.19.0', runGit }); - - expect(changedFiles).toEqual(['src/packages/contracts/index.ts', 'src/index.ts']); - }); - - it('creates a dependency-ordered release plan', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.', dependencies: ['@koala-ts/contracts'] }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - - const releasePlan = createReleasePlan({ - changedFiles: ['package.json', 'src/packages/contracts/index.ts'], - packages, - }); - - expect(releasePlan).toEqual({ packageNames: ['@koala-ts/contracts', '@koala-ts/framework'] }); - }); - - it('includes a package that has not been published', () => { - const packages: Package[] = [ - { name: '@koala-ts/framework', path: '.' }, - { name: '@koala-ts/contracts', path: 'src/packages/contracts' }, - ]; - const getPublishedVersion = (packageName: string) => (packageName === '@koala-ts/contracts' ? undefined : '2.18.1'); - - const unpublishedPackageNames = getUnpublishedPackageNames({ getPublishedVersion, packages }); - - expect(unpublishedPackageNames).toEqual(['@koala-ts/contracts']); - expect(createReleasePlan({ changedFiles: [], packages, unpublishedPackageNames })).toEqual({ - packageNames: ['@koala-ts/contracts'], - }); - }); -}); diff --git a/scripts/release/release-plan.ts b/scripts/release/release-plan.ts deleted file mode 100644 index 678e32d..0000000 --- a/scripts/release/release-plan.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { selectChangedPackages, sortPackagesForPublishing } from './package-selection.ts'; - -/** - * @typedef {object} Package - * @property {string} name - * @property {string} path - * @property {string[]} [dependencies] - */ - -/** - * @param {{ changedFiles: string[]; packages: Package[]; releaseMode?: 'all'; unpublishedPackageNames?: string[] }} options - * @returns {{ packageNames: string[] }} - */ -export const createReleasePlan = ({ changedFiles, packages, releaseMode, unpublishedPackageNames = [] }) => { - const selectedPackageNames = [ - ...new Set([...selectChangedPackages({ changedFiles, packages, releaseMode }), ...unpublishedPackageNames]), - ]; - - return { - packageNames: sortPackagesForPublishing({ packages, selectedPackageNames }), - }; -}; - -/** - * @param {{ releaseTag: string; runGit: (arguments_: string[]) => string }} options - * @returns {string} - */ -export const findPreviousTag = ({ releaseTag, runGit }) => - runGit(['describe', '--tags', '--abbrev=0', `${releaseTag}^`]).trim(); - -/** - * @param {{ previousTag: string; releaseTag: string; runGit: (arguments_: string[]) => string }} options - * @returns {string[]} - */ -export const getChangedFiles = ({ previousTag, releaseTag, runGit }) => - runGit(['diff', '--name-only', `${previousTag}..${releaseTag}`]) - .trim() - .split('\n') - .filter(Boolean); - -/** - * @param {{ packages: Package[]; getPublishedVersion: (packageName: string) => string | undefined }} options - * @returns {string[]} - */ -export const getUnpublishedPackageNames = ({ packages, getPublishedVersion }) => - packages.filter(({ name }) => !getPublishedVersion(name)).map(({ name }) => name); diff --git a/scripts/release/release-staging.test.ts b/scripts/release/release-staging.test.ts deleted file mode 100644 index 0f52e33..0000000 --- a/scripts/release/release-staging.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { type PackageManifest, stagePackageManifests } from './release-staging.ts'; - -describe('release staging', () => { - it('applies the release version to selected packages', () => { - const manifests: PackageManifest[] = [{ name: '@koala-ts/contracts', version: '2.0.0' }]; - - const stagedManifests = stagePackageManifests({ - manifests, - selectedPackageNames: ['@koala-ts/contracts'], - version: '2.18.1', - }); - - expect(stagedManifests).toEqual([{ name: '@koala-ts/contracts', version: '2.18.1' }]); - }); - - it('uses the release version for selected internal dependencies', () => { - const manifests: PackageManifest[] = [ - { name: '@koala-ts/contracts', version: '2.0.0' }, - { name: '@koala-ts/framework', version: '2.0.0', dependencies: { '@koala-ts/contracts': '*' } }, - ]; - - const stagedManifests = stagePackageManifests({ - manifests, - selectedPackageNames: ['@koala-ts/contracts', '@koala-ts/framework'], - version: '2.18.1', - }); - - expect(stagedManifests[1]).toEqual({ - name: '@koala-ts/framework', - version: '2.18.1', - dependencies: { '@koala-ts/contracts': '^2.18.1' }, - }); - }); - - it('uses the installed version for an unchanged internal dependency', () => { - const manifests: PackageManifest[] = [ - { name: '@koala-ts/contracts', version: '2.0.0' }, - { name: '@koala-ts/framework', version: '2.0.0', dependencies: { '@koala-ts/contracts': '*' } }, - ]; - - const stagedManifests = stagePackageManifests({ - manifests, - selectedPackageNames: ['@koala-ts/framework'], - publishedVersions: { '@koala-ts/contracts': '2.17.0' }, - version: '2.18.1', - }); - - expect(stagedManifests[1]).toEqual({ - name: '@koala-ts/framework', - version: '2.18.1', - dependencies: { '@koala-ts/contracts': '^2.17.0' }, - }); - }); -}); diff --git a/scripts/release/release-staging.ts b/scripts/release/release-staging.ts deleted file mode 100644 index 49e48d0..0000000 --- a/scripts/release/release-staging.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @typedef {object} PackageManifest - * @property {string} name - * @property {string} version - * @property {Record} [dependencies] - */ - -/** - * @param {{ - * manifests: PackageManifest[]; - * selectedPackageNames: string[]; - * publishedVersions?: Record; - * version: string; - * }} options - * @returns {PackageManifest[]} - */ -export const stagePackageManifests = ({ manifests, selectedPackageNames, publishedVersions = {}, version }) => - manifests.map(manifest => { - if (!selectedPackageNames.includes(manifest.name)) { - return manifest; - } - - const dependencies = stageDependencies({ - dependencies: manifest.dependencies, - publishedVersions, - selectedPackageNames, - version, - }); - - return { - ...manifest, - version, - ...(dependencies ? { dependencies } : {}), - }; - }); - -/** - * @param {{ - * dependencies?: Record; - * publishedVersions: Record; - * selectedPackageNames: string[]; - * version: string; - * }} options - * @returns {Record | undefined} - */ -const stageDependencies = ({ dependencies, publishedVersions, selectedPackageNames, version }) => { - if (!dependencies) { - return undefined; - } - - return Object.fromEntries( - Object.entries(dependencies).map(([dependency, dependencyVersion]) => { - if (!dependency.startsWith('@koala-ts/')) { - return [dependency, dependencyVersion]; - } - - if (selectedPackageNames.includes(dependency)) { - return [dependency, `^${version}`]; - } - - const publishedVersion = publishedVersions[dependency]; - - if (!publishedVersion) { - throw new Error(`No published version is available for ${dependency}.`); - } - - return [dependency, `^${publishedVersion}`]; - }), - ); -}; diff --git a/scripts/release/stage-release.ts b/scripts/release/stage-release.ts deleted file mode 100644 index 3fa8248..0000000 --- a/scripts/release/stage-release.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; - -import { stagePackageManifests } from './release-staging.ts'; - -const [releaseTag, releasePlanPath] = process.argv.slice(2); - -if (!releaseTag || !releasePlanPath) { - throw new Error('A release tag and release plan path are required.'); -} - -const releasePlan = JSON.parse(readFileSync(releasePlanPath, 'utf8')); -const packageRecords = [readPackageRecord('.'), ...readWorkspacePackageRecords()]; -const selectedPackageNames = releasePlan.packageNames; -const publishedVersions = getPublishedVersions({ packageRecords, selectedPackageNames }); -const stagedManifests = stagePackageManifests({ - manifests: packageRecords.map(({ manifest }) => manifest), - publishedVersions, - selectedPackageNames, - version: releaseTag, -}); - -for (const packageRecord of packageRecords) { - if (!selectedPackageNames.includes(packageRecord.manifest.name)) { - continue; - } - - const stagedManifest = stagedManifests.find(({ name }) => name === packageRecord.manifest.name); - writeFileSync(packageRecord.manifestPath, `${JSON.stringify(stagedManifest, null, 2)}\n`); -} - -function readWorkspacePackageRecords() { - return readdirSync('src/packages', { withFileTypes: true }) - .filter(directoryEntry => directoryEntry.isDirectory()) - .map(directoryEntry => readPackageRecord(join('src/packages', directoryEntry.name))); -} - -function readPackageRecord(path) { - const manifestPath = path === '.' ? 'package.json' : join(path, 'package.json'); - const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); - - return { manifest, manifestPath }; -} - -function getPublishedVersions({ packageRecords, selectedPackageNames }) { - const unchangedDependencies = packageRecords - .filter(({ manifest }) => selectedPackageNames.includes(manifest.name)) - .flatMap(({ manifest }) => Object.keys(manifest.dependencies ?? {})) - .filter(dependency => dependency.startsWith('@koala-ts/') && !selectedPackageNames.includes(dependency)); - - return Object.fromEntries( - [...new Set(unchangedDependencies)].map(dependency => [ - dependency, - execFileSync('npm', ['view', dependency, 'version'], { encoding: 'utf8' }).trim(), - ]), - ); -} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index a987e64..0000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "allowImportingTsExtensions": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "types": ["node"] - }, - "include": ["./**/*.ts"] -} diff --git a/src/packages/contracts/.github/workflows/publish.yml b/src/packages/contracts/.github/workflows/publish.yml new file mode 100644 index 0000000..d217ed3 --- /dev/null +++ b/src/packages/contracts/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +name: Publish + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + dry_run: + description: Validate the package without publishing it + required: true + default: true + type: boolean + +permissions: + contents: read + id-token: write + +jobs: + publish: + uses: koala-ts/framework/.github/workflows/publish-component.yml@2.x + with: + framework_ref: 2.x + dry_run: ${{ inputs.dry_run || false }} + secrets: inherit diff --git a/src/packages/contracts/package.json b/src/packages/contracts/package.json index f534ddb..b2263fd 100644 --- a/src/packages/contracts/package.json +++ b/src/packages/contracts/package.json @@ -4,8 +4,7 @@ "description": "Shared contracts for KoalaTs packages", "repository": { "type": "git", - "url": "git+https://github.com/koala-ts/framework.git", - "directory": "src/packages/contracts" + "url": "git+https://github.com/koala-ts/contracts.git" }, "type": "module", "main": "./dist/index.js", @@ -37,5 +36,10 @@ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "publint": "^0.3.21", + "typescript": "^7.0.2" } } diff --git a/templates/component-release/tsconfig.json b/templates/component-release/tsconfig.json new file mode 100644 index 0000000..0111a1e --- /dev/null +++ b/templates/component-release/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "target": "ESNext", + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "rootDir": ".", + "outDir": "./dist", + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./*.ts"], + "exclude": ["./*.test.ts", "./dist"] +}