From 229365d68af4f007594b51f5ba53221ebf600e3a Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 2 Jan 2026 12:19:36 +0100 Subject: [PATCH 1/4] chore: setup agents.md --- AGENTS.md | 881 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..56808ed --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,881 @@ +# AGENTS.md - Release Scripts Architecture Guide + +## Project Overview + +**@ucdjs/release-scripts** is a sophisticated monorepo release automation library built on Effect-TS. It provides programmatic tools for managing releases in pnpm workspaces with automated version calculation, dependency graph resolution, and GitHub integration. + +### Key Features + +- **Automated Version Calculation**: Analyzes git commits using conventional commit standards to determine appropriate version bumps (major, minor, patch) +- **Workspace Management**: Discovers and manages multiple packages in pnpm workspaces +- **Dependency Graph Resolution**: Computes topological ordering for package releases based on workspace dependencies +- **GitHub Integration**: Creates and manages release pull requests, sets commit statuses +- **Release Verification**: Validates that release branches match expected release artifacts +- **Dry-Run Support**: Allows testing release workflows without making actual changes + +### Current Status + +Version: `v0.1.0-beta.24` + +Implemented workflows: +- ✅ `verify()` - Release branch verification +- ✅ `prepare()` - Release preparation with version updates +- ⏳ `publish()` - NPM publishing (planned) + +## Architecture + +### Effect-TS Functional Architecture + +The codebase uses **Effect-TS** as its foundational architecture, providing: + +#### Service Pattern +All services extend `Effect.Service` with dependency injection: + +```typescript +export class ServiceName extends Effect.Service()( + "service-identifier", + { effect: Effect.gen(function* () { ... }), dependencies: [...] } +) {} +``` + +#### Key Architectural Benefits + +1. **Dependency Injection**: Effect Layers compose services with automatic dependency resolution +2. **Error Handling**: Tagged errors using `Data.TaggedError` for type-safe error handling +3. **Generator-based Control Flow**: Clean async operations without callback hell +4. **Composability**: Services can be combined and tested in isolation + +#### Error Handling Strategy + +Custom error types in `src/errors.ts`: +- `GitError`: Git command failures +- `PackageNotFoundError`: Missing workspace packages +- `WorkspaceError`: Workspace discovery/validation failures +- `GitHubError`: GitHub API failures +- `CircularDependencyError`: Circular dependencies in dependency graph + +## Service Architecture + +All services are located in `src/services/` and follow the Effect-TS service pattern. + +### GitService +**File**: `src/services/git.service.ts` + +**Purpose**: Wrapper for git command execution + +**Key Operations**: +- Branch management (`create`, `checkout`, `list`, `exists`) +- Commit operations (`stage`, `write`, `push`) +- Tag retrieval (`getMostRecentPackageTag`) +- File reading from specific refs (`readFile`) +- Commit history fetching (via `commit-parser`) +- Working directory validation + +**Dry-Run Support**: All state-changing operations check `config.dryRun` and skip execution + +**Dependencies**: +- `@effect/platform` CommandExecutor +- `commit-parser` for parsing conventional commits + +### GitHubService +**File**: `src/services/github.service.ts` + +**Purpose**: GitHub API client with authentication + +**Key Operations**: +- Pull request operations (`getPullRequestByBranch`, `createPullRequest`, `updatePullRequest`) +- Commit status management (`setCommitStatus`) +- Schema validation using Effect Schema + +**API Response Schemas**: +- `PullRequest`: Validates PR structure (number, title, body, state, head SHA) +- `CommitStatus`: Validates commit status responses + +**Supported PR States**: open, closed, merged + +**Authentication**: Uses GitHub token from configuration + +### WorkspaceService +**File**: `src/services/workspace.service.ts` + +**Purpose**: Package discovery and management in pnpm workspaces + +**Key Operations**: +- Discovers packages using `pnpm -r ls --json` +- Reads/writes package.json files +- Filters packages by include/exclude lists +- Identifies workspace dependencies vs external dependencies +- Supports private package exclusion + +**Caching**: Caches workspace package list for performance + +**Package Structure**: +```typescript +interface WorkspacePackage { + name: string; + version: string; + path: string; + private?: boolean; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; +} +``` + +### VersionCalculatorService +**File**: `src/services/version-calculator.service.ts` + +**Purpose**: Determines version bumps from conventional commits + +**Bump Rules**: +- Breaking changes (`BREAKING CHANGE:`, `!`) → **major** +- Features (`feat:`) → **minor** +- Fixes/Performance (`fix:`, `perf:`) → **patch** +- Other commits → **none** + +**Bump Priority**: none < patch < minor < major + +**Features**: +- Supports version overrides from configuration +- Distinguishes between direct changes vs dependency updates +- Calculates bump type for dependency-only updates + +### DependencyGraphService +**File**: `src/services/dependency-graph.service.ts` + +**Purpose**: Builds and analyzes package dependency graphs + +**Key Operations**: +- Builds dependency graph from workspace packages +- Computes topological ordering using Kahn's algorithm +- Detects circular dependencies with detailed error messages +- Assigns "level" to each package based on dependency depth + +**Level Assignment**: +- Level 0: No workspace dependencies +- Level N: Depends on packages at level N-1 + +**Error Handling**: Throws `CircularDependencyError` with full cycle path + +### PackageUpdaterService +**File**: `src/services/package-updater.service.ts` + +**Purpose**: Applies version bumps to package.json files + +**Key Operations**: +- Applies version bumps to package.json files +- Updates dependency ranges (dependencies, devDependencies, peerDependencies) +- Handles `workspace:` protocol ranges +- Preserves semver range prefixes (`^`, `~`) +- Validates that new versions satisfy existing ranges + +**Complex Range Handling**: +- Validates complex ranges before updating +- Throws error if update would break existing range constraints +- Supports workspace: protocol with version suffixes + +### NPMService +**File**: `src/services/npm.service.ts` + +**Purpose**: NPM registry client + +**Key Operations**: +- Fetches package metadata (packument) from NPM registry +- Checks if specific versions exist on NPM +- Retrieves latest published version +- 404 handling for unpublished packages + +**Registry**: Defaults to `https://registry.npmjs.org` + +## Helper Utilities + +**File**: `src/utils/helpers.ts` + +### loadOverrides() +Reads version override configuration from `.github/ucdjs-release.overrides.json` + +**Override Structure**: +```json +{ + "overrides": { + "@package/name": { + "version": "1.2.3", + "dependencies": { + "dependency-name": "^2.0.0" + } + } + } +} +``` + +### mergePackageCommitsIntoPackages() +Enriches packages with commits since last tag + +**Process**: +1. Gets most recent tag for each package +2. Fetches commits since that tag +3. Filters commits by package path +4. Merges commits into package metadata + +### mergeCommitsAffectingGloballyIntoPackage() +Sophisticated global commit attribution + +**Modes**: +- `"none"`: No global commits attributed +- `"all"`: All global commits attributed to all packages +- `"dependencies"`: Only dependency-related global commits attributed + +**Smart Attribution**: +- Prevents double-counting commits across package releases +- Timestamp-based filtering to attribute global commits correctly +- Handles edge case: pkg-a released → global change → pkg-b released + +**Global Commit Detection**: +- Commits affecting files outside package directories +- Root-level dependency files (package.json, pnpm-lock.yaml) +- Workspace-wide configuration files + +### isGlobalCommit() +Identifies commits affecting files outside package directories + +### isDependencyFile() +Detects dependency-related files: +- `package.json` +- `pnpm-lock.yaml` +- `pnpm-workspace.yaml` + +### findCommitRange() +Finds oldest/newest commits across all packages for changelog generation + +## Main Entry Point + +**File**: `src/index.ts` + +### API Interface + +```typescript +export interface ReleaseScripts { + verify: () => Promise; // Verify release branch integrity + prepare: () => Promise; // Prepare release (calculate & update versions) + publish: () => Promise; // Publish to NPM (not yet implemented) + packages: { + list: () => Promise; + get: (packageName: string) => Promise; + }; +} + +export async function createReleaseScripts( + options: ReleaseScriptsOptionsInput +): Promise +``` + +### Initialization Flow + +1. **Normalize Options**: Validates and normalizes configuration +2. **Construct Effect Layer**: Builds dependency injection layer with all services +3. **Validate Workspace**: Ensures valid git repository and clean working directory +4. **Return API**: Provides typed API for release operations + +### Configuration Options + +```typescript +interface ReleaseScriptsOptionsInput { + repo: string; // "owner/repo" + githubToken?: string; // GitHub API token + workspaceRoot?: string; // Path to workspace root + packages?: { + include?: string[]; // Package name filters + exclude?: string[]; // Package name exclusions + excludePrivate?: boolean; // Exclude private packages + }; + branch?: { + release?: string; // Release branch name + default?: string; // Default/main branch + }; + globalCommitMode?: "none" | "all" | "dependencies"; + dryRun?: boolean; // Enable dry-run mode +} +``` + +## Core Workflows + +### verify() - Release Verification + +**Purpose**: Ensures release branch matches expected release artifacts + +**Process**: +1. Fetches release PR by branch name +2. Loads version overrides from `.github/ucdjs-release.overrides.json` +3. Discovers workspace packages (filtered by config) +4. Merges package-specific and global commits +5. Calculates expected version bumps for all packages +6. Reads package.json files from release branch HEAD +7. Compares expected vs actual versions/dependencies +8. Reports drift (version mismatches, dependency range issues) +9. Sets GitHub commit status (success/failure) + +**Exit Codes**: +- 0: All packages match expected state +- 1: Drift detected or errors occurred + +**Implementation**: `src/verify.ts` + +### prepare() - Release Preparation + +**Purpose**: Prepares release by calculating and applying version updates + +**Process**: +1. Fetches release PR (or prepares to create one) +2. Loads version overrides +3. Discovers packages and enriches with commits +4. Calculates version bumps for all packages +5. Computes topological order (dependency-aware) +6. Applies releases (updates package.json files) +7. Creates/updates PR (future enhancement) +8. Generates changelogs (future enhancement) + +**Current State**: Updates package.json files locally + +**Future Enhancements**: +- PR creation/update automation +- Changelog generation +- Release notes formatting + +### publish() - NPM Publishing + +**Status**: Not yet implemented + +**Planned Features**: +- Publish packages to NPM in topological order +- Respect private package flags +- Provenance generation +- Tag creation after successful publish + +## Workflow Diagrams + +### verify() Workflow + +```mermaid +flowchart TD + Start([Start verify]) --> FetchPR[Fetch Release PR by branch] + FetchPR --> CheckPR{PR exists?} + CheckPR -->|No| ErrorNoPR[Error: No release PR found] + CheckPR -->|Yes| LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] + + LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] + DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + + FilterPkgs --> GetTags[Get most recent tag
for each package] + GetTags --> FetchCommits[Fetch commits since tag
for each package] + + FetchCommits --> MergeCommits[Merge package-specific commits] + MergeCommits --> GlobalCommits{Global commit
mode?} + + GlobalCommits -->|none| CalcVersions[Calculate expected versions
from commits] + GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] + GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + + MergeGlobalAll --> CalcVersions + MergeGlobalDeps --> CalcVersions + + CalcVersions --> ApplyOverrides[Apply version overrides
from config] + ApplyOverrides --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> CheckCircular{Circular
dependencies?} + + CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] + CheckCircular -->|No| TopoSort[Compute topological order] + + TopoSort --> ReadActual[Read package.json files
from release branch HEAD] + ReadActual --> Compare[Compare expected vs actual
versions & dependencies] + + Compare --> CheckDrift{Drift detected?} + CheckDrift -->|Yes| ReportDrift[Report drift details
versions/dependencies] + CheckDrift -->|No| ReportSuccess[Report: All packages match] + + ReportDrift --> SetStatusFail[Set GitHub commit status
state: failure] + ReportSuccess --> SetStatusSuccess[Set GitHub commit status
state: success] + + SetStatusFail --> Exit1([Exit code 1]) + SetStatusSuccess --> Exit0([Exit code 0]) + + ErrorNoPR --> Exit1 + ErrorCircular --> Exit1 + + style Start fill:#e1f5e1 + style Exit0 fill:#e1f5e1 + style Exit1 fill:#ffe1e1 + style ErrorNoPR fill:#ffcccc + style ErrorCircular fill:#ffcccc + style CheckDrift fill:#fff4e1 + style SetStatusSuccess fill:#d4edda + style SetStatusFail fill:#f8d7da +``` + +### prepare() Workflow + +```mermaid +flowchart TD + Start([Start prepare]) --> FetchPR[Fetch/Prepare Release PR] + FetchPR --> LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] + + LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] + DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + + FilterPkgs --> GetTags[Get most recent tag
for each package] + GetTags --> FetchCommits[Fetch commits since tag
for each package] + + FetchCommits --> MergeCommits[Merge package-specific commits] + MergeCommits --> GlobalCommits{Global commit
mode?} + + GlobalCommits -->|none| CalcVersions[Calculate version bumps
from commits] + GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] + GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + + MergeGlobalAll --> CalcVersions + MergeGlobalDeps --> CalcVersions + + CalcVersions --> ApplyOverrides[Apply version overrides
from config] + ApplyOverrides --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> CheckCircular{Circular
dependencies?} + + CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] + CheckCircular -->|No| TopoSort[Compute topological order
with levels] + + TopoSort --> LoopPkgs{More packages
to update?} + LoopPkgs -->|Yes| NextPkg[Get next package
in topo order] + + NextPkg --> ApplyVersion[Update package version
in package.json] + ApplyVersion --> UpdateDeps[Update workspace dependencies
preserve ranges & workspace: protocol] + + UpdateDeps --> DryRun{Dry-run
mode?} + DryRun -->|Yes| LogUpdate[Log: Would update package.json] + DryRun -->|No| WriteFile[Write package.json to disk] + + LogUpdate --> LoopPkgs + WriteFile --> LoopPkgs + + LoopPkgs -->|No| Future[Future: Create/Update PR
Generate changelogs] + Future --> Success([Success]) + + ErrorCircular --> Exit1([Exit code 1]) + + style Start fill:#e1f5e1 + style Success fill:#e1f5e1 + style Exit1 fill:#ffe1e1 + style ErrorCircular fill:#ffcccc + style DryRun fill:#fff4e1 + style Future fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 +``` + +### publish() Workflow (Planned) + +```mermaid +flowchart TD + Start([Start publish]) --> DiscoverPkgs[Discover workspace packages] + DiscoverPkgs --> FilterPrivate[Filter out private packages] + + FilterPrivate --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> TopoSort[Compute topological order
with levels] + + TopoSort --> GroupByLevel[Group packages by level
for parallel publishing] + + GroupByLevel --> LoopLevels{More levels
to publish?} + LoopLevels -->|Yes| NextLevel[Get next level] + + NextLevel --> ParallelPub[Publish packages in parallel
within level] + + ParallelPub --> LoopPkgsInLevel{More packages
in level?} + LoopPkgsInLevel -->|Yes| NextPkg[Get next package] + + NextPkg --> CheckNPM[Check if version exists on NPM
NPMService.getPackument] + CheckNPM --> Exists{Version
exists?} + + Exists -->|Yes| SkipPkg[Skip: Already published] + Exists -->|No| BuildPkg[Build package
pnpm build --filter] + + BuildPkg --> PublishPkg[Publish to NPM
pnpm publish --provenance] + PublishPkg --> CreateTag[Create git tag
@package/name@version] + CreateTag --> PushTag[Push tag to remote] + + PushTag --> LoopPkgsInLevel + SkipPkg --> LoopPkgsInLevel + + LoopPkgsInLevel -->|No| WaitLevel[Wait for all packages
in level to complete] + WaitLevel --> LoopLevels + + LoopLevels -->|No| UpdatePR[Update Release PR
with publish results] + UpdatePR --> Success([Success]) + + style Start fill:#e1f5e1 + style Success fill:#e1f5e1 + style ParallelPub fill:#fff4e1 + style PublishPkg fill:#d4edda + style SkipPkg fill:#f0f0f0 + style Start stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 + style Success stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 +``` + +### Service Dependency Graph + +```mermaid +graph TD + Config[Configuration
ReleaseScriptsOptions] + + Git[GitService
git commands] + GitHub[GitHubService
GitHub API] + Workspace[WorkspaceService
package discovery] + VersionCalc[VersionCalculatorService
version bumps] + DepGraph[DependencyGraphService
topological sort] + PkgUpdater[PackageUpdaterService
package.json updates] + NPM[NPMService
NPM registry] + + Helpers[Helper Utilities
commit attribution] + + Config --> Git + Config --> GitHub + Config --> Workspace + + Git --> Workspace + Workspace --> VersionCalc + Workspace --> DepGraph + + Git --> Helpers + Workspace --> Helpers + + Helpers --> VersionCalc + VersionCalc --> PkgUpdater + DepGraph --> PkgUpdater + Workspace --> PkgUpdater + + Workspace --> NPM + + GitHub -.-> |used by verify|Verify[verify.ts] + Git -.-> |used by verify|Verify + Workspace -.-> |used by verify|Verify + VersionCalc -.-> |used by verify|Verify + DepGraph -.-> |used by verify|Verify + Helpers -.-> |used by verify|Verify + + Git -.-> |used by prepare|Prepare[prepare flow] + Workspace -.-> |used by prepare|Prepare + VersionCalc -.-> |used by prepare|Prepare + DepGraph -.-> |used by prepare|Prepare + PkgUpdater -.-> |used by prepare|Prepare + Helpers -.-> |used by prepare|Prepare + + NPM -.-> |used by publish|Publish[publish flow
planned] + Git -.-> |used by publish|Publish + Workspace -.-> |used by publish|Publish + DepGraph -.-> |used by publish|Publish + + style Config fill:#e1f0ff + style Git fill:#ffe1e1 + style GitHub fill:#ffe1e1 + style Workspace fill:#e1ffe1 + style VersionCalc fill:#fff4e1 + style DepGraph fill:#fff4e1 + style PkgUpdater fill:#f0e1ff + style NPM fill:#ffe1e1 + style Helpers fill:#f0f0f0 + style Verify fill:#d4edda + style Prepare fill:#d4edda + style Publish fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 +``` + +### Commit Attribution Flow + +```mermaid +flowchart TD + Start([Packages with commits]) --> HasGlobal{Global commit
mode?} + + HasGlobal -->|none| NoGlobal[No global commits added] + HasGlobal -->|all| GetAllGlobal[Get all global commits
from main branch] + HasGlobal -->|dependencies| GetDepGlobal[Get dependency-related
global commits] + + GetAllGlobal --> FindRange[Find commit time range
across all packages] + GetDepGlobal --> FindRange + + FindRange --> FilterTime[Filter global commits
by timestamp range] + FilterTime --> LoopPkgs{More packages?} + + LoopPkgs -->|Yes| NextPkg[Get next package] + NextPkg --> GetLastRelease[Get last release timestamp
from most recent tag] + + GetLastRelease --> FilterCommits[Filter global commits:
commit.time > lastRelease.time] + FilterCommits --> MergeInto[Merge filtered commits
into package] + + MergeInto --> LoopPkgs + LoopPkgs -->|No| Result([Packages with attributed commits]) + NoGlobal --> Result + + style Start fill:#e1f5e1 + style Result fill:#e1f5e1 + style HasGlobal fill:#fff4e1 + style FilterCommits fill:#d4edda +``` + +### Version Bump Calculation + +```mermaid +flowchart TD + Start([Package with commits]) --> HasOverride{Has version
override?} + + HasOverride -->|Yes| UseOverride[Use overridden version] + HasOverride -->|No| CheckCommits{Has commits?} + + CheckCommits -->|No| NoBump[Bump type: none] + CheckCommits -->|Yes| AnalyzeCommits[Analyze each commit] + + AnalyzeCommits --> LoopCommits{More commits?} + LoopCommits -->|Yes| NextCommit[Get next commit] + + NextCommit --> CheckBreaking{Breaking change?
BREAKING CHANGE:
or !} + CheckBreaking -->|Yes| Major[Bump type: major] + CheckBreaking -->|No| CheckFeat{Feature?
feat:} + + CheckFeat -->|Yes| Minor[Bump type: minor] + CheckFeat -->|No| CheckFix{Fix or perf?
fix: or perf:} + + CheckFix -->|Yes| Patch[Bump type: patch] + CheckFix -->|No| Other[No bump for this commit] + + Major --> UpdateMax[Update max bump type] + Minor --> UpdateMax + Patch --> UpdateMax + Other --> LoopCommits + + UpdateMax --> LoopCommits + LoopCommits -->|No| ApplyBump[Apply highest bump type
to current version] + + ApplyBump --> Result([New version]) + UseOverride --> Result + NoBump --> Result + + style Start fill:#e1f5e1 + style Result fill:#e1f5e1 + style Major fill:#ffcccc + style Minor fill:#fff4cc + style Patch fill:#ccffcc + style NoBump fill:#f0f0f0 + style UseOverride fill:#e1f0ff +``` + +## Technology Stack + +### Core Dependencies + +**Effect-TS Ecosystem**: +- `effect@3.19.9`: Functional effect system +- `@effect/platform@0.93.6`: Platform abstractions +- `@effect/platform-node@0.103.0`: Node.js implementations +- Effect Schema for runtime validation +- Effect Layer for dependency injection + +**Git & Commit Analysis**: +- `commit-parser@1.3.0`: Conventional commit parsing +- Native git commands via CommandExecutor + +**Package Management**: +- `pnpm` workspace integration +- `semver@7.7.3`: Semantic versioning utilities + +**Utilities**: +- `@luxass/utils@2.7.2`: General utilities +- `farver@1.0.0-beta.1`: Color utilities +- `mri@1.2.0`: CLI argument parsing +- `prompts@2.4.2`: Interactive prompts +- `tinyexec@1.0.2`: Lightweight process execution + +### Development Dependencies + +**Build Tooling**: +- `tsdown@0.17.0`: TypeScript bundler (Rolldown-based) +- `typescript@5.9.3` + +**Testing**: +- `vitest@4.0.15`: Test runner +- `@effect/vitest@0.27.0`: Effect-specific test utilities +- `vitest-testdirs@4.3.0`: Test directory management + +**Linting**: +- `eslint@9.39.1` +- `@luxass/eslint-config@6.0.3`: Custom ESLint configuration + +**Template Engine**: +- `eta@4.4.1`: JavaScript templating (for changelogs/PR bodies) + +## Build & Development + +### Build Process + +**Configuration**: `tsdown.config.ts` + +**Output**: +- `dist/index.mjs`: Bundled ESM +- `dist/index.d.mts`: TypeScript declarations +- Separate chunk for `eta` template engine + +**Features**: +- Tree-shaking enabled +- DTS generation +- ESM-only output +- Advanced chunking + +### Scripts + +```bash +pnpm build # Build production bundle +pnpm dev # Watch mode for development +pnpm test # Run Vitest tests +pnpm lint # Run ESLint +pnpm typecheck # TypeScript type checking +``` + +### TypeScript Configuration + +**Target**: ES2022 +**Module**: ESNext with Bundler resolution +**Strict Mode**: Enabled with `noUncheckedIndexedAccess` +**Effect Plugin**: Language service integration enabled + +### Import Aliases + +```typescript +#services/* → ./src/services/*.service.ts +``` + +## Testing Strategy + +**Framework**: Vitest with Effect test utilities + +**Coverage Areas**: +- Helper functions (commit classification, range finding) +- Options normalization +- NPM service mocking +- Effect Layer composition + +**Test Utilities**: +- `@effect/vitest`: Effect-aware test helpers +- `vitest-testdirs`: Isolated test workspaces + +## CI/CD Workflows + +### CI Pipeline +**File**: `.github/workflows/ci.yml` + +**Triggers**: Push to main, Pull requests + +**Steps**: +1. Checkout +2. Setup Node.js and pnpm +3. Install dependencies (frozen lockfile) +4. Build +5. Lint +6. Type check + +### Release Pipeline +**File**: `.github/workflows/release.yml` + +**Triggers**: Tag push (`v*`) + +**Steps**: +1. Checkout with full git history +2. Generate changelog (changelogithub) +3. Install dependencies and build +4. Detect tag type (pre-release vs stable) +5. Publish to NPM with provenance +6. NPM OIDC authentication + +## Key Architectural Patterns + +### 1. Command Execution Abstraction +- Git/shell commands via `@effect/platform` CommandExecutor +- Automatic error mapping to custom error types +- Working directory management + +### 2. Schema Validation +- Effect Schema for runtime validation +- GitHub API responses validated against schemas +- Package.json structure validation + +### 3. Dry-Run Mode +- State-changing operations check `config.dryRun` +- Git commands that modify state return success messages instead +- File writes skipped in dry-run mode + +### 4. Commit Attribution Algorithm +- Sophisticated timestamp-based filtering for global commits +- Prevents double-counting across releases +- Three modes: "none", "all", "dependencies" +- Handles edge case: pkg-a released → global change → pkg-b released + +### 5. Dependency Graph Topological Sort +- Kahn's algorithm for cycle detection +- Level assignment for parallel release potential +- Clear error messages for circular dependencies + +### 6. Semver Range Updating +- Preserves `workspace:` protocol +- Maintains range prefixes (`^`, `~`) +- Validates complex ranges +- Throws if new version breaks existing range constraints + +### 7. Monorepo-Aware Design +- Package-scoped git tags (`@package/name@1.0.0`) +- Workspace dependency tracking +- Global vs package-specific commit distinction + +## File Structure + +``` +src/ +├── services/ # Effect-based services +│ ├── git.service.ts # Git operations +│ ├── github.service.ts # GitHub API +│ ├── workspace.service.ts # Package discovery +│ ├── version-calculator.service.ts # Version bump logic +│ ├── dependency-graph.service.ts # Topological sorting +│ ├── package-updater.service.ts # package.json updates +│ └── npm.service.ts # NPM registry client +├── utils/ +│ └── helpers.ts # Global commit handling, overrides +├── index.ts # Main entry point & API +├── options.ts # Configuration normalization +├── errors.ts # Custom error types +└── verify.ts # Release verification program +``` + +## Future Enhancements + +### Short-term +- Complete `publish()` workflow implementation +- PR creation/update automation in `prepare()` +- Changelog generation using `eta` templates + +### Medium-term +- Interactive mode with `prompts` integration +- Custom commit parsers beyond conventional commits +- Configurable tag formats + +### Long-term +- Support for other package managers (npm, yarn) +- Plugin system for custom release workflows +- Release notes generation with AI summarization + +## Contributing + +When working on this codebase: + +1. **Understand Effect-TS**: Familiarize yourself with Effect-TS patterns +2. **Service Pattern**: Follow the established service pattern for new features +3. **Error Handling**: Use tagged errors for type-safe error handling +4. **Dry-Run**: Ensure state-changing operations support dry-run mode +5. **Testing**: Add tests for new functionality using Vitest +6. **Type Safety**: Leverage TypeScript strict mode and `noUncheckedIndexedAccess` + +## References + +- [Effect-TS Documentation](https://effect.website/) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [pnpm Workspaces](https://pnpm.io/workspaces) +- [GitHub REST API](https://docs.github.com/en/rest) From 8a0817ccf224c9174e36850d1c1ff0ce044f5cb3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 2 Jan 2026 12:32:33 +0100 Subject: [PATCH 2/4] chore(docs): add workflow and architecture diagrams to AGENTS.md Added Mermaid diagrams for various workflows and architecture in the `docs/diagrams/` directory. Updated `AGENTS.md` to reference these diagrams for better visualization of the release scripts architecture and processes. --- AGENTS.md | 20 +++ docs/diagrams/README.md | 150 +++++++++++++++++++++ docs/diagrams/commit-attribution-flow.mmd | 27 ++++ docs/diagrams/prepare-workflow.mmd | 51 +++++++ docs/diagrams/publish-workflow.mmd | 43 ++++++ docs/diagrams/service-dependency-graph.mmd | 62 +++++++++ docs/diagrams/verify-workflow.mmd | 53 ++++++++ docs/diagrams/version-bump-calculation.mmd | 41 ++++++ src/services/github.service.ts | 23 ++++ src/verify.ts | 123 ++++++++++++++++- 10 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 docs/diagrams/README.md create mode 100644 docs/diagrams/commit-attribution-flow.mmd create mode 100644 docs/diagrams/prepare-workflow.mmd create mode 100644 docs/diagrams/publish-workflow.mmd create mode 100644 docs/diagrams/service-dependency-graph.mmd create mode 100644 docs/diagrams/verify-workflow.mmd create mode 100644 docs/diagrams/version-bump-calculation.mmd diff --git a/AGENTS.md b/AGENTS.md index 56808ed..fb405e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -353,8 +353,18 @@ interface ReleaseScriptsOptionsInput { ## Workflow Diagrams +> **Note**: All diagrams are also available as separate Mermaid files in [`docs/diagrams/`](../docs/diagrams/) for reuse and image generation: +> - [verify-workflow.mmd](../docs/diagrams/verify-workflow.mmd) +> - [prepare-workflow.mmd](../docs/diagrams/prepare-workflow.mmd) +> - [publish-workflow.mmd](../docs/diagrams/publish-workflow.mmd) +> - [service-dependency-graph.mmd](../docs/diagrams/service-dependency-graph.mmd) +> - [commit-attribution-flow.mmd](../docs/diagrams/commit-attribution-flow.mmd) +> - [version-bump-calculation.mmd](../docs/diagrams/version-bump-calculation.mmd) + ### verify() Workflow +**Diagram**: [`docs/diagrams/verify-workflow.mmd`](../docs/diagrams/verify-workflow.mmd) + ```mermaid flowchart TD Start([Start verify]) --> FetchPR[Fetch Release PR by branch] @@ -413,6 +423,8 @@ flowchart TD ### prepare() Workflow +**Diagram**: [`docs/diagrams/prepare-workflow.mmd`](../docs/diagrams/prepare-workflow.mmd) + ```mermaid flowchart TD Start([Start prepare]) --> FetchPR[Fetch/Prepare Release PR] @@ -469,6 +481,8 @@ flowchart TD ### publish() Workflow (Planned) +**Diagram**: [`docs/diagrams/publish-workflow.mmd`](../docs/diagrams/publish-workflow.mmd) + ```mermaid flowchart TD Start([Start publish]) --> DiscoverPkgs[Discover workspace packages] @@ -517,6 +531,8 @@ flowchart TD ### Service Dependency Graph +**Diagram**: [`docs/diagrams/service-dependency-graph.mmd`](../docs/diagrams/service-dependency-graph.mmd) + ```mermaid graph TD Config[Configuration
ReleaseScriptsOptions] @@ -584,6 +600,8 @@ graph TD ### Commit Attribution Flow +**Diagram**: [`docs/diagrams/commit-attribution-flow.mmd`](../docs/diagrams/commit-attribution-flow.mmd) + ```mermaid flowchart TD Start([Packages with commits]) --> HasGlobal{Global commit
mode?} @@ -616,6 +634,8 @@ flowchart TD ### Version Bump Calculation +**Diagram**: [`docs/diagrams/version-bump-calculation.mmd`](../docs/diagrams/version-bump-calculation.mmd) + ```mermaid flowchart TD Start([Package with commits]) --> HasOverride{Has version
override?} diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md new file mode 100644 index 0000000..624a98d --- /dev/null +++ b/docs/diagrams/README.md @@ -0,0 +1,150 @@ +# Release Scripts Diagrams + +This directory contains Mermaid diagram files (`.mmd`) that visualize the architecture and workflows of the @ucdjs/release-scripts project. + +## Recommended Viewing Order + +For the best understanding of the system, view the diagrams in this order: + +### 1. Architecture Overview +Start with the overall architecture to understand how services interact: + +**[service-dependency-graph.mmd](./service-dependency-graph.mmd)** +- Shows all 7 services and their dependencies +- Illustrates how configuration flows into services +- Displays which services are used by each workflow (verify, prepare, publish) +- Color-coded by service type + +### 2. Supporting Algorithms +Before diving into workflows, understand the key algorithms: + +**[version-bump-calculation.mmd](./version-bump-calculation.mmd)** +- How version bumps are calculated from conventional commits +- Shows the priority: breaking changes → major, features → minor, fixes → patch +- Includes version override handling + +**[commit-attribution-flow.mmd](./commit-attribution-flow.mmd)** +- Sophisticated global commit attribution algorithm +- Three modes: none, all, dependencies +- Timestamp-based filtering to prevent double-counting +- Critical for understanding how commits are attributed in monorepos + +### 3. Main Workflows +Now examine the three main workflows in order of complexity: + +**[verify-workflow.mmd](./verify-workflow.mmd)** (Implemented) +- Verifies that release branch matches expected state +- Loads overrides, discovers packages, calculates expected versions +- Compares expected vs actual versions/dependencies +- Sets GitHub commit status (success/failure) +- **Use case**: CI/CD validation of release PRs + +**[prepare-workflow.mmd](./prepare-workflow.mmd)** (Implemented) +- Prepares releases by calculating and applying version updates +- Updates package.json files in topological order +- Supports dry-run mode +- Shows future enhancements (PR creation, changelog generation) +- **Use case**: Local release preparation before creating/updating PR + +**[publish-workflow.mmd](./publish-workflow.mmd)** (Planned) +- Publishing packages to NPM in topological order +- Parallel publishing within dependency levels +- NPM version existence checking +- Git tag creation after successful publish +- **Use case**: Automated NPM publishing from CI/CD + +## Diagram Files + +| File | Type | Purpose | Status | +|------|------|---------|--------| +| `service-dependency-graph.mmd` | Architecture | Service dependencies and workflow usage | Current | +| `version-bump-calculation.mmd` | Algorithm | Version bump logic from commits | Current | +| `commit-attribution-flow.mmd` | Algorithm | Global commit attribution | Current | +| `verify-workflow.mmd` | Workflow | Release branch verification | Implemented | +| `prepare-workflow.mmd` | Workflow | Release preparation | Implemented | +| `publish-workflow.mmd` | Workflow | NPM publishing | Planned | + +## Viewing the Diagrams + +### Online (GitHub) +GitHub automatically renders `.mmd` files when you view them in the web interface. Simply click on any diagram file above. + +### Locally with VS Code +Install the [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension to view diagrams in markdown preview. + +### Generate PNG/SVG Images +Use the [Mermaid CLI](https://github.com/mermaid-js/mermaid-cli) to generate images: + +```bash +# Install mermaid-cli +npm install -g @mermaid-js/mermaid-cli + +# Generate PNG +mmdc -i verify-workflow.mmd -o verify-workflow.png + +# Generate SVG +mmdc -i verify-workflow.mmd -o verify-workflow.svg + +# Generate all diagrams +for file in *.mmd; do mmdc -i "$file" -o "${file%.mmd}.png"; done +``` + +### Online Editor +Copy and paste diagram content into the [Mermaid Live Editor](https://mermaid.live/) for interactive viewing and editing. + +## Color Coding + +The diagrams use consistent color coding: + +### Workflow Diagrams +- **Green** (`#e1f5e1`): Start/Success states +- **Red** (`#ffe1e1`, `#ffcccc`): Error states and exit failures +- **Yellow** (`#fff4e1`): Decision points and important checks +- **Blue dashed** (`#e1f0ff` with dashed border): Planned/future features +- **Light green** (`#d4edda`): Successful operations +- **Light red** (`#f8d7da`): Failed operations +- **Gray** (`#f0f0f0`): Skip/neutral operations + +### Service Dependency Graph +- **Light blue** (`#e1f0ff`): Configuration +- **Light red** (`#ffe1e1`): External services (Git, GitHub, NPM) +- **Light green** (`#e1ffe1`): Core workspace service +- **Yellow** (`#fff4e1`): Calculation services +- **Purple** (`#f0e1ff`): Update services +- **Gray** (`#f0f0f0`): Helper utilities +- **Green** (`#d4edda`): Implemented workflows +- **Blue dashed**: Planned workflows + +### Version Bump Calculation +- **Red** (`#ffcccc`): Major bump +- **Yellow** (`#fff4cc`): Minor bump +- **Green** (`#ccffcc`): Patch bump +- **Gray** (`#f0f0f0`): No bump +- **Blue** (`#e1f0ff`): Override + +## Integration + +These diagrams are referenced in the main project documentation: +- **[AGENTS.md](../../AGENTS.md)**: Complete architecture guide with inline diagram previews +- Each workflow section in AGENTS.md links to the corresponding diagram file + +## Updating Diagrams + +When updating diagrams: + +1. Edit the `.mmd` file directly +2. Test the diagram syntax at [Mermaid Live Editor](https://mermaid.live/) +3. Ensure color coding follows the conventions above +4. Update both the `.mmd` file and the corresponding inline diagram in AGENTS.md +5. Consider regenerating PNG/SVG images if they're used elsewhere + +## Contributing + +When adding new diagrams: + +1. Follow the naming convention: `kebab-case.mmd` +2. Add the diagram to this README with description and purpose +3. Reference it from AGENTS.md if applicable +4. Use consistent color coding (see above) +5. Include clear labels and decision points +6. Test rendering on GitHub before committing diff --git a/docs/diagrams/commit-attribution-flow.mmd b/docs/diagrams/commit-attribution-flow.mmd new file mode 100644 index 0000000..e12a53e --- /dev/null +++ b/docs/diagrams/commit-attribution-flow.mmd @@ -0,0 +1,27 @@ +flowchart TD + Start([Packages with commits]) --> HasGlobal{Global commit
mode?} + + HasGlobal -->|none| NoGlobal[No global commits added] + HasGlobal -->|all| GetAllGlobal[Get all global commits
from main branch] + HasGlobal -->|dependencies| GetDepGlobal[Get dependency-related
global commits] + + GetAllGlobal --> FindRange[Find commit time range
across all packages] + GetDepGlobal --> FindRange + + FindRange --> FilterTime[Filter global commits
by timestamp range] + FilterTime --> LoopPkgs{More packages?} + + LoopPkgs -->|Yes| NextPkg[Get next package] + NextPkg --> GetLastRelease[Get last release timestamp
from most recent tag] + + GetLastRelease --> FilterCommits[Filter global commits:
commit.time > lastRelease.time] + FilterCommits --> MergeInto[Merge filtered commits
into package] + + MergeInto --> LoopPkgs + LoopPkgs -->|No| Result([Packages with attributed commits]) + NoGlobal --> Result + + style Start fill:#e1f5e1 + style Result fill:#e1f5e1 + style HasGlobal fill:#fff4e1 + style FilterCommits fill:#d4edda diff --git a/docs/diagrams/prepare-workflow.mmd b/docs/diagrams/prepare-workflow.mmd new file mode 100644 index 0000000..a312606 --- /dev/null +++ b/docs/diagrams/prepare-workflow.mmd @@ -0,0 +1,51 @@ +flowchart TD + Start([Start prepare]) --> FetchPR[Fetch/Prepare Release PR] + FetchPR --> LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] + + LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] + DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + + FilterPkgs --> GetTags[Get most recent tag
for each package] + GetTags --> FetchCommits[Fetch commits since tag
for each package] + + FetchCommits --> MergeCommits[Merge package-specific commits] + MergeCommits --> GlobalCommits{Global commit
mode?} + + GlobalCommits -->|none| CalcVersions[Calculate version bumps
from commits] + GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] + GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + + MergeGlobalAll --> CalcVersions + MergeGlobalDeps --> CalcVersions + + CalcVersions --> ApplyOverrides[Apply version overrides
from config] + ApplyOverrides --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> CheckCircular{Circular
dependencies?} + + CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] + CheckCircular -->|No| TopoSort[Compute topological order
with levels] + + TopoSort --> LoopPkgs{More packages
to update?} + LoopPkgs -->|Yes| NextPkg[Get next package
in topo order] + + NextPkg --> ApplyVersion[Update package version
in package.json] + ApplyVersion --> UpdateDeps[Update workspace dependencies
preserve ranges & workspace: protocol] + + UpdateDeps --> DryRun{Dry-run
mode?} + DryRun -->|Yes| LogUpdate[Log: Would update package.json] + DryRun -->|No| WriteFile[Write package.json to disk] + + LogUpdate --> LoopPkgs + WriteFile --> LoopPkgs + + LoopPkgs -->|No| Future[Future: Create/Update PR
Generate changelogs] + Future --> Success([Success]) + + ErrorCircular --> Exit1([Exit code 1]) + + style Start fill:#e1f5e1 + style Success fill:#e1f5e1 + style Exit1 fill:#ffe1e1 + style ErrorCircular fill:#ffcccc + style DryRun fill:#fff4e1 + style Future fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 diff --git a/docs/diagrams/publish-workflow.mmd b/docs/diagrams/publish-workflow.mmd new file mode 100644 index 0000000..fb6aea0 --- /dev/null +++ b/docs/diagrams/publish-workflow.mmd @@ -0,0 +1,43 @@ +flowchart TD + Start([Start publish]) --> DiscoverPkgs[Discover workspace packages] + DiscoverPkgs --> FilterPrivate[Filter out private packages] + + FilterPrivate --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> TopoSort[Compute topological order
with levels] + + TopoSort --> GroupByLevel[Group packages by level
for parallel publishing] + + GroupByLevel --> LoopLevels{More levels
to publish?} + LoopLevels -->|Yes| NextLevel[Get next level] + + NextLevel --> ParallelPub[Publish packages in parallel
within level] + + ParallelPub --> LoopPkgsInLevel{More packages
in level?} + LoopPkgsInLevel -->|Yes| NextPkg[Get next package] + + NextPkg --> CheckNPM[Check if version exists on NPM
NPMService.getPackument] + CheckNPM --> Exists{Version
exists?} + + Exists -->|Yes| SkipPkg[Skip: Already published] + Exists -->|No| BuildPkg[Build package
pnpm build --filter] + + BuildPkg --> PublishPkg[Publish to NPM
pnpm publish --provenance] + PublishPkg --> CreateTag[Create git tag
@package/name@version] + CreateTag --> PushTag[Push tag to remote] + + PushTag --> LoopPkgsInLevel + SkipPkg --> LoopPkgsInLevel + + LoopPkgsInLevel -->|No| WaitLevel[Wait for all packages
in level to complete] + WaitLevel --> LoopLevels + + LoopLevels -->|No| UpdatePR[Update Release PR
with publish results] + UpdatePR --> Success([Success]) + + style Start fill:#e1f5e1 + style Success fill:#e1f5e1 + style ParallelPub fill:#fff4e1 + style PublishPkg fill:#d4edda + style SkipPkg fill:#f0f0f0 + style Start stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 + style Success stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 diff --git a/docs/diagrams/service-dependency-graph.mmd b/docs/diagrams/service-dependency-graph.mmd new file mode 100644 index 0000000..dd9431d --- /dev/null +++ b/docs/diagrams/service-dependency-graph.mmd @@ -0,0 +1,62 @@ +graph TD + Config[Configuration
ReleaseScriptsOptions] + + Git[GitService
git commands] + GitHub[GitHubService
GitHub API] + Workspace[WorkspaceService
package discovery] + VersionCalc[VersionCalculatorService
version bumps] + DepGraph[DependencyGraphService
topological sort] + PkgUpdater[PackageUpdaterService
package.json updates] + NPM[NPMService
NPM registry] + + Helpers[Helper Utilities
commit attribution] + + Config --> Git + Config --> GitHub + Config --> Workspace + + Git --> Workspace + Workspace --> VersionCalc + Workspace --> DepGraph + + Git --> Helpers + Workspace --> Helpers + + Helpers --> VersionCalc + VersionCalc --> PkgUpdater + DepGraph --> PkgUpdater + Workspace --> PkgUpdater + + Workspace --> NPM + + GitHub -.-> |used by verify|Verify[verify.ts] + Git -.-> |used by verify|Verify + Workspace -.-> |used by verify|Verify + VersionCalc -.-> |used by verify|Verify + DepGraph -.-> |used by verify|Verify + Helpers -.-> |used by verify|Verify + + Git -.-> |used by prepare|Prepare[prepare flow] + Workspace -.-> |used by prepare|Prepare + VersionCalc -.-> |used by prepare|Prepare + DepGraph -.-> |used by prepare|Prepare + PkgUpdater -.-> |used by prepare|Prepare + Helpers -.-> |used by prepare|Prepare + + NPM -.-> |used by publish|Publish[publish flow
planned] + Git -.-> |used by publish|Publish + Workspace -.-> |used by publish|Publish + DepGraph -.-> |used by publish|Publish + + style Config fill:#e1f0ff + style Git fill:#ffe1e1 + style GitHub fill:#ffe1e1 + style Workspace fill:#e1ffe1 + style VersionCalc fill:#fff4e1 + style DepGraph fill:#fff4e1 + style PkgUpdater fill:#f0e1ff + style NPM fill:#ffe1e1 + style Helpers fill:#f0f0f0 + style Verify fill:#d4edda + style Prepare fill:#d4edda + style Publish fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 diff --git a/docs/diagrams/verify-workflow.mmd b/docs/diagrams/verify-workflow.mmd new file mode 100644 index 0000000..067c02f --- /dev/null +++ b/docs/diagrams/verify-workflow.mmd @@ -0,0 +1,53 @@ +flowchart TD + Start([Start verify]) --> FetchPR[Fetch Release PR by branch] + FetchPR --> CheckPR{PR exists?} + CheckPR -->|No| ErrorNoPR[Error: No release PR found] + CheckPR -->|Yes| LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] + + LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] + DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + + FilterPkgs --> GetTags[Get most recent tag
for each package] + GetTags --> FetchCommits[Fetch commits since tag
for each package] + + FetchCommits --> MergeCommits[Merge package-specific commits] + MergeCommits --> GlobalCommits{Global commit
mode?} + + GlobalCommits -->|none| CalcVersions[Calculate expected versions
from commits] + GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] + GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + + MergeGlobalAll --> CalcVersions + MergeGlobalDeps --> CalcVersions + + CalcVersions --> ApplyOverrides[Apply version overrides
from config] + ApplyOverrides --> BuildDepGraph[Build dependency graph] + BuildDepGraph --> CheckCircular{Circular
dependencies?} + + CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] + CheckCircular -->|No| TopoSort[Compute topological order] + + TopoSort --> ReadActual[Read package.json files
from release branch HEAD] + ReadActual --> Compare[Compare expected vs actual
versions & dependencies] + + Compare --> CheckDrift{Drift detected?} + CheckDrift -->|Yes| ReportDrift[Report drift details
versions/dependencies] + CheckDrift -->|No| ReportSuccess[Report: All packages match] + + ReportDrift --> SetStatusFail[Set GitHub commit status
state: failure] + ReportSuccess --> SetStatusSuccess[Set GitHub commit status
state: success] + + SetStatusFail --> Exit1([Exit code 1]) + SetStatusSuccess --> Exit0([Exit code 0]) + + ErrorNoPR --> Exit1 + ErrorCircular --> Exit1 + + style Start fill:#e1f5e1 + style Exit0 fill:#e1f5e1 + style Exit1 fill:#ffe1e1 + style ErrorNoPR fill:#ffcccc + style ErrorCircular fill:#ffcccc + style CheckDrift fill:#fff4e1 + style SetStatusSuccess fill:#d4edda + style SetStatusFail fill:#f8d7da diff --git a/docs/diagrams/version-bump-calculation.mmd b/docs/diagrams/version-bump-calculation.mmd new file mode 100644 index 0000000..3aa5ced --- /dev/null +++ b/docs/diagrams/version-bump-calculation.mmd @@ -0,0 +1,41 @@ +flowchart TD + Start([Package with commits]) --> HasOverride{Has version
override?} + + HasOverride -->|Yes| UseOverride[Use overridden version] + HasOverride -->|No| CheckCommits{Has commits?} + + CheckCommits -->|No| NoBump[Bump type: none] + CheckCommits -->|Yes| AnalyzeCommits[Analyze each commit] + + AnalyzeCommits --> LoopCommits{More commits?} + LoopCommits -->|Yes| NextCommit[Get next commit] + + NextCommit --> CheckBreaking{Breaking change?
BREAKING CHANGE:
or !} + CheckBreaking -->|Yes| Major[Bump type: major] + CheckBreaking -->|No| CheckFeat{Feature?
feat:} + + CheckFeat -->|Yes| Minor[Bump type: minor] + CheckFeat -->|No| CheckFix{Fix or perf?
fix: or perf:} + + CheckFix -->|Yes| Patch[Bump type: patch] + CheckFix -->|No| Other[No bump for this commit] + + Major --> UpdateMax[Update max bump type] + Minor --> UpdateMax + Patch --> UpdateMax + Other --> LoopCommits + + UpdateMax --> LoopCommits + LoopCommits -->|No| ApplyBump[Apply highest bump type
to current version] + + ApplyBump --> Result([New version]) + UseOverride --> Result + NoBump --> Result + + style Start fill:#e1f5e1 + style Result fill:#e1f5e1 + style Major fill:#ffcccc + style Minor fill:#fff4cc + style Patch fill:#ccffcc + style NoBump fill:#f0f0f0 + style UseOverride fill:#e1f0ff diff --git a/src/services/github.service.ts b/src/services/github.service.ts index c585e97..edc2508 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -114,8 +114,31 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea ); } + function setCommitStatus(sha: string, status: CommitStatus) { + return makeRequest( + `statuses/${sha}`, + Schema.Unknown, + { + method: "POST", + body: JSON.stringify(status), + }, + ).pipe( + Effect.map(() => status), + Effect.catchAll((e) => + Effect.fail( + new GitHubError({ + message: e.message, + operation: "setCommitStatus", + cause: e.cause, + }), + ), + ), + ); + } + return { getPullRequestByBranch, + setCommitStatus, } as const; }), dependencies: [], diff --git a/src/verify.ts b/src/verify.ts index 5a72218..0e92b46 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -1,3 +1,4 @@ +import type { WorkspacePackage } from "#services/workspace"; import type { NormalizedReleaseScriptsOptions } from "./options"; import { DependencyGraphService } from "#services/dependency-graph"; import { GitService } from "#services/git"; @@ -5,12 +6,101 @@ import { GitHubService } from "#services/github"; import { VersionCalculatorService } from "#services/version-calculator"; import { WorkspaceService } from "#services/workspace"; import { Console, Effect } from "effect"; +import semver from "semver"; import { loadOverrides, mergeCommitsAffectingGloballyIntoPackage, mergePackageCommitsIntoPackages, } from "./utils/helpers"; +interface DriftReason { + readonly packageName: string; + readonly reason: string; +} + +function satisfiesRange(range: string, version: string): boolean { + // For simple ranges, use semver.satisfies. For complex ranges, semver still works; + // we accept ranges that already include the new version. + return semver.satisfies(version, range, { includePrerelease: true }); +} + +function snapshotPackageJson(pkg: WorkspacePackage, ref: string) { + return Effect.gen(function* () { + const git = yield* GitService; + + return yield* git.workspace.readFile(`${pkg.path}/package.json`, ref).pipe( + Effect.flatMap((content) => Effect.try({ + try: () => JSON.parse(content) as Record, + catch: (e) => new Error(`Failed to parse package.json for ${pkg.name} at ${ref}: ${String(e)}`), + })), + ); + }); +} + +function findDrift( + packages: readonly WorkspacePackage[], + releases: readonly { + package: WorkspacePackage; + newVersion: string; + }[], + branchSnapshots: Map | Error>, +): DriftReason[] { + const releaseVersionByName = new Map(); + for (const rel of releases) { + releaseVersionByName.set(rel.package.name, rel.newVersion); + } + + const reasons: DriftReason[] = []; + + for (const pkg of packages) { + const snapshot = branchSnapshots.get(pkg.name); + if (snapshot == null) { + reasons.push({ packageName: pkg.name, reason: "package.json missing on release branch" }); + continue; + } + + if (snapshot instanceof Error) { + reasons.push({ packageName: pkg.name, reason: snapshot.message }); + continue; + } + + const expectedVersion = releaseVersionByName.get(pkg.name) ?? pkg.version; + const branchVersion = typeof snapshot.version === "string" ? snapshot.version : undefined; + + if (!branchVersion) { + reasons.push({ packageName: pkg.name, reason: "package.json on release branch lacks version" }); + continue; + } + + if (branchVersion !== expectedVersion) { + reasons.push({ packageName: pkg.name, reason: `version mismatch: expected ${expectedVersion}, found ${branchVersion}` }); + } + + // Check workspace dependency ranges for updated packages + const dependencySections = ["dependencies", "devDependencies", "peerDependencies"] as const; + for (const section of dependencySections) { + const deps = snapshot[section]; + if (!deps || typeof deps !== "object") continue; + + for (const [depName, range] of Object.entries(deps as Record)) { + const bumpedVersion = releaseVersionByName.get(depName); + if (!bumpedVersion) continue; + + if (typeof range !== "string") { + reasons.push({ packageName: pkg.name, reason: `${section}.${depName} is not a string range` }); + continue; + } + + if (!satisfiesRange(range, bumpedVersion)) { + reasons.push({ packageName: pkg.name, reason: `${section}.${depName} does not include ${bumpedVersion}` }); + } + } + } + } + + return reasons; +} + export function constructVerifyProgram( config: NormalizedReleaseScriptsOptions, ) { @@ -56,9 +146,34 @@ export function constructVerifyProgram( yield* Console.log("Calculated releases:", releases); yield* Console.log("Release order:", ordered); - // STEP 4: Calculate the updates - // STEP 5: Read package.jsons from release branch (without checkout) - // STEP 6: Detect if Release PR is out of sync - // STEP 7: Set Commit Status + const releaseHeadSha = releasePullRequest.head.sha; + + const branchSnapshots = new Map | Error>(); + for (const pkg of packages) { + const snapshot = yield* snapshotPackageJson(pkg, releaseHeadSha).pipe( + Effect.catchAll((err) => Effect.succeed(err instanceof Error ? err : new Error(String(err)))), + ); + branchSnapshots.set(pkg.name, snapshot); + } + + const drift = findDrift(packages, releases, branchSnapshots); + + if (drift.length === 0) { + yield* Console.log("✅ Release branch is in sync with expected releases."); + } else { + yield* Console.log("❌ Release branch is out of sync:", drift); + } + + const status = drift.length === 0 + ? { state: "success" as const, description: "Release artifacts in sync", context: "release/verify" } + : { state: "failure" as const, description: "Release branch out of sync", context: "release/verify" }; + + yield* github.setCommitStatus(releaseHeadSha, status); + + if (drift.length > 0) { + return yield* Effect.fail(new Error("Release branch is out of sync.")); + } + + return true; }); } From a01050b4f7f64644b09db489eb781488bb7f5400 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 2 Jan 2026 14:04:37 +0100 Subject: [PATCH 3/4] docs: enhance AGENTS.md and diagram files with detailed descriptions Updated `AGENTS.md` to include diagram types for clarity. Added new `package-lifecycle.mmd` and improved existing diagrams in `commit-attribution-flow.mmd`, `prepare-workflow.mmd`, `publish-workflow.mmd`, and `service-dependency-graph.mmd` to reflect service interactions and workflows more accurately. --- AGENTS.md | 31 ++-- docs/diagrams/commit-attribution-flow.mmd | 78 +++++--- docs/diagrams/package-lifecycle.mmd | 80 +++++++++ docs/diagrams/prepare-workflow.mmd | 136 ++++++++++---- docs/diagrams/publish-workflow.mmd | 101 +++++++---- docs/diagrams/service-dependency-graph.mmd | 198 ++++++++++++++------- docs/diagrams/verify-workflow.mmd | 139 ++++++++++----- 7 files changed, 559 insertions(+), 204 deletions(-) create mode 100644 docs/diagrams/package-lifecycle.mmd diff --git a/AGENTS.md b/AGENTS.md index fb405e4..7e24454 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -354,16 +354,18 @@ interface ReleaseScriptsOptionsInput { ## Workflow Diagrams > **Note**: All diagrams are also available as separate Mermaid files in [`docs/diagrams/`](../docs/diagrams/) for reuse and image generation: -> - [verify-workflow.mmd](../docs/diagrams/verify-workflow.mmd) -> - [prepare-workflow.mmd](../docs/diagrams/prepare-workflow.mmd) -> - [publish-workflow.mmd](../docs/diagrams/publish-workflow.mmd) -> - [service-dependency-graph.mmd](../docs/diagrams/service-dependency-graph.mmd) -> - [commit-attribution-flow.mmd](../docs/diagrams/commit-attribution-flow.mmd) -> - [version-bump-calculation.mmd](../docs/diagrams/version-bump-calculation.mmd) +> - [verify-workflow.mmd](../docs/diagrams/verify-workflow.mmd) - Sequence diagram +> - [prepare-workflow.mmd](../docs/diagrams/prepare-workflow.mmd) - Sequence diagram +> - [publish-workflow.mmd](../docs/diagrams/publish-workflow.mmd) - Sequence diagram +> - [service-dependency-graph.mmd](../docs/diagrams/service-dependency-graph.mmd) - Class diagram +> - [commit-attribution-flow.mmd](../docs/diagrams/commit-attribution-flow.mmd) - Sequence diagram +> - [version-bump-calculation.mmd](../docs/diagrams/version-bump-calculation.mmd) - Flowchart +> - [package-lifecycle.mmd](../docs/diagrams/package-lifecycle.mmd) - State diagram ### verify() Workflow -**Diagram**: [`docs/diagrams/verify-workflow.mmd`](../docs/diagrams/verify-workflow.mmd) +**Diagram Type**: Sequence diagram showing service interactions +**File**: [`docs/diagrams/verify-workflow.mmd`](../docs/diagrams/verify-workflow.mmd) ```mermaid flowchart TD @@ -423,7 +425,8 @@ flowchart TD ### prepare() Workflow -**Diagram**: [`docs/diagrams/prepare-workflow.mmd`](../docs/diagrams/prepare-workflow.mmd) +**Diagram Type**: Sequence diagram showing service interactions +**File**: [`docs/diagrams/prepare-workflow.mmd`](../docs/diagrams/prepare-workflow.mmd) ```mermaid flowchart TD @@ -481,7 +484,8 @@ flowchart TD ### publish() Workflow (Planned) -**Diagram**: [`docs/diagrams/publish-workflow.mmd`](../docs/diagrams/publish-workflow.mmd) +**Diagram Type**: Sequence diagram showing service interactions +**File**: [`docs/diagrams/publish-workflow.mmd`](../docs/diagrams/publish-workflow.mmd) ```mermaid flowchart TD @@ -531,7 +535,8 @@ flowchart TD ### Service Dependency Graph -**Diagram**: [`docs/diagrams/service-dependency-graph.mmd`](../docs/diagrams/service-dependency-graph.mmd) +**Diagram Type**: Class diagram showing structure and relationships +**File**: [`docs/diagrams/service-dependency-graph.mmd`](../docs/diagrams/service-dependency-graph.mmd) ```mermaid graph TD @@ -600,7 +605,8 @@ graph TD ### Commit Attribution Flow -**Diagram**: [`docs/diagrams/commit-attribution-flow.mmd`](../docs/diagrams/commit-attribution-flow.mmd) +**Diagram Type**: Sequence diagram showing the attribution algorithm +**File**: [`docs/diagrams/commit-attribution-flow.mmd`](../docs/diagrams/commit-attribution-flow.mmd) ```mermaid flowchart TD @@ -634,7 +640,8 @@ flowchart TD ### Version Bump Calculation -**Diagram**: [`docs/diagrams/version-bump-calculation.mmd`](../docs/diagrams/version-bump-calculation.mmd) +**Diagram Type**: Flowchart showing decision logic +**File**: [`docs/diagrams/version-bump-calculation.mmd`](../docs/diagrams/version-bump-calculation.mmd) ```mermaid flowchart TD diff --git a/docs/diagrams/commit-attribution-flow.mmd b/docs/diagrams/commit-attribution-flow.mmd index e12a53e..d2ebac6 100644 --- a/docs/diagrams/commit-attribution-flow.mmd +++ b/docs/diagrams/commit-attribution-flow.mmd @@ -1,27 +1,65 @@ -flowchart TD - Start([Packages with commits]) --> HasGlobal{Global commit
mode?} +sequenceDiagram + participant API as Main Workflow + participant Helpers as Helper Utils + participant Git as GitService + participant Packages as Package List - HasGlobal -->|none| NoGlobal[No global commits added] - HasGlobal -->|all| GetAllGlobal[Get all global commits
from main branch] - HasGlobal -->|dependencies| GetDepGlobal[Get dependency-related
global commits] + Note over API,Packages: Input: Packages with package-specific commits already merged - GetAllGlobal --> FindRange[Find commit time range
across all packages] - GetDepGlobal --> FindRange + API->>Helpers: mergeCommitsAffectingGloballyIntoPackage(packages, mode) - FindRange --> FilterTime[Filter global commits
by timestamp range] - FilterTime --> LoopPkgs{More packages?} + alt mode = "none" + Helpers-->>API: Return packages unchanged + Note over API: No global commits added + else mode = "all" or "dependencies" + Note over Helpers,Git: 1. Get all commits from main branch + Helpers->>Packages: findCommitRange(packages) + Packages-->>Helpers: {oldest, newest} commit timestamps - LoopPkgs -->|Yes| NextPkg[Get next package] - NextPkg --> GetLastRelease[Get last release timestamp
from most recent tag] + Helpers->>Git: getCommitsSince(oldestTag) + Git-->>Helpers: allCommits[] - GetLastRelease --> FilterCommits[Filter global commits:
commit.time > lastRelease.time] - FilterCommits --> MergeInto[Merge filtered commits
into package] + Note over Helpers: 2. Filter for global commits + loop For each commit + Helpers->>Helpers: isGlobalCommit(commit, packages) + alt Commit affects files outside package dirs + Helpers->>Helpers: Mark as global commit + alt mode = "dependencies" + Helpers->>Helpers: isDependencyFile(commit.files) + opt Is dependency file + Helpers->>Helpers: Keep commit + end + else mode = "all" + Helpers->>Helpers: Keep commit + end + end + end - MergeInto --> LoopPkgs - LoopPkgs -->|No| Result([Packages with attributed commits]) - NoGlobal --> Result + Note over Helpers: 3. Attribute commits to packages + loop For each package + Helpers->>Git: getMostRecentPackageTag(package) + Git-->>Helpers: lastReleaseTag - style Start fill:#e1f5e1 - style Result fill:#e1f5e1 - style HasGlobal fill:#fff4e1 - style FilterCommits fill:#d4edda + Helpers->>Git: getCommitsSince(lastReleaseTag) + Git-->>Helpers: lastReleaseCommits[] + + Helpers->>Helpers: Find newest commit timestamp + Note over Helpers: lastReleaseTime = newest commit time + + Note over Helpers: 4. Filter global commits by timestamp + loop For each global commit + alt commit.time > lastReleaseTime + Helpers->>Helpers: Attribute to package + Note over Helpers: This prevents double-counting:
commit happened AFTER this
package was last released + else commit.time <= lastReleaseTime + Helpers->>Helpers: Skip (already counted in previous release) + end + end + + Helpers->>Packages: Merge attributed commits into package + end + + Helpers-->>API: packages with global commits added + end + + Note over API: Result: Packages with both package-specific
and attributed global commits diff --git a/docs/diagrams/package-lifecycle.mmd b/docs/diagrams/package-lifecycle.mmd new file mode 100644 index 0000000..dcec513 --- /dev/null +++ b/docs/diagrams/package-lifecycle.mmd @@ -0,0 +1,80 @@ +stateDiagram-v2 + [*] --> Discovered: Package found in workspace + + Discovered --> Analyzing: Has commits since last tag + Discovered --> NoChanges: No commits since last tag + + NoChanges --> [*]: Skip release + + Analyzing --> ParsingCommits: Load commits + ParsingCommits --> GlobalCommitAttribution: Package commits parsed + + GlobalCommitAttribution --> CalculatingBump: Global commits attributed + + CalculatingBump --> CheckOverride: Analyze conventional commits + CheckOverride --> OverrideApplied: Override exists + CheckOverride --> BumpCalculated: No override + + OverrideApplied --> PendingRelease: Version set from override + BumpCalculated --> NoBump: No significant changes + BumpCalculated --> PendingRelease: Bump type determined + + NoBump --> [*]: Skip release + + PendingRelease --> InDependencyGraph: Add to graph + InDependencyGraph --> CircularCheck: Analyze dependencies + + CircularCheck --> Error: Circular dependency detected + CircularCheck --> TopoSorted: No cycles found + + Error --> [*]: Release fails + + TopoSorted --> WaitingForDeps: Assigned level in graph + WaitingForDeps --> ReadyToUpdate: Dependencies updated + + note right of ReadyToUpdate + Packages at Level 0 skip this wait. + Others wait for lower levels. + end note + + ReadyToUpdate --> UpdatingVersion: Turn in topological order + UpdatingVersion --> UpdatingDependencies: Version bumped in package.json + UpdatingDependencies --> ValidatingRanges: Workspace deps updated + + ValidatingRanges --> RangeError: Invalid semver range + ValidatingRanges --> DryRunCheck: Ranges valid + + RangeError --> [*]: Release fails + + DryRunCheck --> LoggedUpdate: Dry-run mode + DryRunCheck --> WrittenToDisk: Normal mode + + LoggedUpdate --> Updated: Simulated update + WrittenToDisk --> Updated: package.json written + + Updated --> Verifying: verify() called + + Verifying --> DriftDetected: Expected ≠ Actual + Verifying --> Verified: Expected = Actual + + DriftDetected --> [*]: Verification fails + + Verified --> AwaitingPublish: Ready for NPM + AwaitingPublish --> CheckingNPM: publish() called + + CheckingNPM --> AlreadyPublished: Version exists on NPM + CheckingNPM --> Building: Version not on NPM + + AlreadyPublished --> [*]: Skip publish + + Building --> Publishing: Build successful + Publishing --> CreatingTag: Published to NPM + CreatingTag --> PushingTag: Git tag created + PushingTag --> Published: Tag pushed to remote + + Published --> [*]: Release complete + + note left of Published + Package is now available on NPM + with a corresponding git tag + end note diff --git a/docs/diagrams/prepare-workflow.mmd b/docs/diagrams/prepare-workflow.mmd index a312606..f328c3a 100644 --- a/docs/diagrams/prepare-workflow.mmd +++ b/docs/diagrams/prepare-workflow.mmd @@ -1,51 +1,113 @@ -flowchart TD - Start([Start prepare]) --> FetchPR[Fetch/Prepare Release PR] - FetchPR --> LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] +sequenceDiagram + actor User + participant API as prepare() + participant GH as GitHubService + participant WS as WorkspaceService + participant Git as GitService + participant Helpers as Helper Utils + participant VC as VersionCalculator + participant DG as DependencyGraph + participant PU as PackageUpdater - LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] - DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + User->>API: Start preparation - FilterPkgs --> GetTags[Get most recent tag
for each package] - GetTags --> FetchCommits[Fetch commits since tag
for each package] + Note over API,GH: 1. Fetch/Prepare Release PR + API->>GH: getPullRequestByBranch(releaseBranch) + alt PR exists + GH-->>API: PR{number, title} + Note over API: Will update existing PR (future) + else No PR + GH-->>API: null + Note over API: Will create new PR (future) + end - FetchCommits --> MergeCommits[Merge package-specific commits] - MergeCommits --> GlobalCommits{Global commit
mode?} + Note over API,Helpers: 2. Load Configuration + API->>Helpers: loadOverrides() + Helpers->>Git: readFile(".github/ucdjs-release.overrides.json") + Git-->>Helpers: overrides JSON + Helpers-->>API: overrides{} - GlobalCommits -->|none| CalcVersions[Calculate version bumps
from commits] - GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] - GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + Note over API,WS: 3. Discover Packages + API->>WS: discoverPackages() + WS->>WS: pnpm -r ls --json + WS->>WS: Filter by include/exclude/private + WS-->>API: packages[] - MergeGlobalAll --> CalcVersions - MergeGlobalDeps --> CalcVersions + Note over API,Git: 4. Fetch Commits for Each Package + loop For each package + API->>Git: getMostRecentPackageTag(pkg) + Git-->>API: tag + API->>Git: getCommitsSince(tag) + Git-->>API: commits[] + API->>API: Filter commits by package path + end - CalcVersions --> ApplyOverrides[Apply version overrides
from config] - ApplyOverrides --> BuildDepGraph[Build dependency graph] - BuildDepGraph --> CheckCircular{Circular
dependencies?} + Note over API,Helpers: 5. Merge Global Commits + alt globalCommitMode != "none" + API->>Helpers: mergeCommitsAffectingGloballyIntoPackage() + Helpers->>Git: getCommitsSince(oldestTag) + Git-->>Helpers: all commits + Helpers->>Helpers: Filter & attribute commits + Helpers-->>API: packages with global commits + end - CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] - CheckCircular -->|No| TopoSort[Compute topological order
with levels] + Note over API,VC: 6. Calculate Version Bumps + loop For each package + API->>VC: calculateBump(package.commits) + VC->>VC: Analyze conventional commits + VC-->>API: bumpType + alt Has override + API->>API: Use override version + else Calculate bump + API->>VC: applyBump(currentVersion, bumpType) + VC-->>API: newVersion + end + end - TopoSort --> LoopPkgs{More packages
to update?} - LoopPkgs -->|Yes| NextPkg[Get next package
in topo order] + Note over API,DG: 7. Build Dependency Graph + API->>DG: buildGraph(packages) + DG->>DG: Analyze workspace dependencies + alt Circular dependency + DG-->>API: CircularDependencyError + API-->>User: Error (exit 1) + else Success + DG-->>API: graph + API->>DG: topologicalSort(graph) + DG-->>API: sortedPackages with levels + end - NextPkg --> ApplyVersion[Update package version
in package.json] - ApplyVersion --> UpdateDeps[Update workspace dependencies
preserve ranges & workspace: protocol] + Note over API,PU: 8. Apply Releases in Topological Order + loop For each package (in topo order) + API->>PU: applyRelease(package, newVersion, allPackages) - UpdateDeps --> DryRun{Dry-run
mode?} - DryRun -->|Yes| LogUpdate[Log: Would update package.json] - DryRun -->|No| WriteFile[Write package.json to disk] + Note over PU: Update package.json + PU->>WS: readPackageJson(package.path) + WS-->>PU: packageJson - LogUpdate --> LoopPkgs - WriteFile --> LoopPkgs + PU->>PU: Set version to newVersion - LoopPkgs -->|No| Future[Future: Create/Update PR
Generate changelogs] - Future --> Success([Success]) + Note over PU: Update workspace dependencies + loop For each workspace dependency + PU->>PU: Find dependency's new version + PU->>PU: Update range (preserve ^/~/workspace:) + PU->>PU: Validate range compatibility + end - ErrorCircular --> Exit1([Exit code 1]) + alt Dry-run mode + PU-->>API: Log: Would update package.json + Note over API: No file written + else Normal mode + PU->>WS: writePackageJson(package.path, updatedJson) + WS->>WS: Write to disk + WS-->>PU: Success + PU-->>API: Package updated + end + end - style Start fill:#e1f5e1 - style Success fill:#e1f5e1 - style Exit1 fill:#ffe1e1 - style ErrorCircular fill:#ffcccc - style DryRun fill:#fff4e1 - style Future fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 + Note over API,Git: 9. Future: Create/Update PR + Note over API: Stage changes (git add) + Note over API: Commit changes + Note over API: Push to release branch + Note over API: Create or update PR with changelog + + API-->>User: Preparation complete diff --git a/docs/diagrams/publish-workflow.mmd b/docs/diagrams/publish-workflow.mmd index fb6aea0..6bdcd5f 100644 --- a/docs/diagrams/publish-workflow.mmd +++ b/docs/diagrams/publish-workflow.mmd @@ -1,43 +1,82 @@ -flowchart TD - Start([Start publish]) --> DiscoverPkgs[Discover workspace packages] - DiscoverPkgs --> FilterPrivate[Filter out private packages] +sequenceDiagram + actor User + participant API as publish() + participant WS as WorkspaceService + participant DG as DependencyGraph + participant NPM as NPMService + participant Git as GitService + participant GH as GitHubService - FilterPrivate --> BuildDepGraph[Build dependency graph] - BuildDepGraph --> TopoSort[Compute topological order
with levels] + User->>API: Start publishing - TopoSort --> GroupByLevel[Group packages by level
for parallel publishing] + Note over API,WS: 1. Discover Packages + API->>WS: discoverPackages() + WS->>WS: pnpm -r ls --json + WS-->>API: packages[] - GroupByLevel --> LoopLevels{More levels
to publish?} - LoopLevels -->|Yes| NextLevel[Get next level] + API->>API: Filter out private packages + Note over API: Private packages are skipped - NextLevel --> ParallelPub[Publish packages in parallel
within level] + Note over API,DG: 2. Build Dependency Graph + API->>DG: buildGraph(packages) + DG->>DG: Analyze workspace dependencies + DG-->>API: graph - ParallelPub --> LoopPkgsInLevel{More packages
in level?} - LoopPkgsInLevel -->|Yes| NextPkg[Get next package] + API->>DG: topologicalSort(graph) + DG-->>API: sortedPackages with levels - NextPkg --> CheckNPM[Check if version exists on NPM
NPMService.getPackument] - CheckNPM --> Exists{Version
exists?} + API->>API: Group packages by level + Note over API: Level 0: No deps
Level 1: Depends on Level 0
etc. - Exists -->|Yes| SkipPkg[Skip: Already published] - Exists -->|No| BuildPkg[Build package
pnpm build --filter] + Note over API,NPM: 3. Publish by Level (for safe ordering) + loop For each level + Note over API: Packages in same level can
publish in parallel - BuildPkg --> PublishPkg[Publish to NPM
pnpm publish --provenance] - PublishPkg --> CreateTag[Create git tag
@package/name@version] - CreateTag --> PushTag[Push tag to remote] + par For each package in level + API->>NPM: getPackument(package.name) + alt Package not on NPM + NPM-->>API: 404 error + Note over API: First publish + else Package exists + NPM-->>API: {versions: {...}} + API->>NPM: Check if version exists + alt Version already published + NPM-->>API: true + Note over API: Skip: Already published + else New version + NPM-->>API: false + end + end - PushTag --> LoopPkgsInLevel - SkipPkg --> LoopPkgsInLevel + opt Version not published yet + Note over API: Build package + API->>WS: Build package (pnpm build --filter) + WS-->>API: Build complete - LoopPkgsInLevel -->|No| WaitLevel[Wait for all packages
in level to complete] - WaitLevel --> LoopLevels + Note over API: Publish to NPM + API->>NPM: Publish (pnpm publish --provenance) + NPM->>NPM: Validate package + NPM->>NPM: Upload to registry + NPM-->>API: Published successfully - LoopLevels -->|No| UpdatePR[Update Release PR
with publish results] - UpdatePR --> Success([Success]) + Note over API: Create git tag + API->>Git: createTag(@package/name@version) + Git-->>API: Tag created - style Start fill:#e1f5e1 - style Success fill:#e1f5e1 - style ParallelPub fill:#fff4e1 - style PublishPkg fill:#d4edda - style SkipPkg fill:#f0f0f0 - style Start stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 - style Success stroke:#4a90e2,stroke-width:3px,stroke-dasharray: 5 5 + API->>Git: pushTag(tag) + Git-->>API: Tag pushed + end + end + + Note over API: Wait for all packages
in level to complete + end + + Note over API,GH: 4. Update Release PR + API->>GH: updatePullRequest(pr.number, publishResults) + GH-->>API: PR updated with results + + Note over API: 5. Summary + API->>API: Collect publish statistics + Note over API: - Packages published
- Packages skipped
- Tags created + + API-->>User: Publishing complete diff --git a/docs/diagrams/service-dependency-graph.mmd b/docs/diagrams/service-dependency-graph.mmd index dd9431d..3b917a2 100644 --- a/docs/diagrams/service-dependency-graph.mmd +++ b/docs/diagrams/service-dependency-graph.mmd @@ -1,62 +1,136 @@ -graph TD - Config[Configuration
ReleaseScriptsOptions] - - Git[GitService
git commands] - GitHub[GitHubService
GitHub API] - Workspace[WorkspaceService
package discovery] - VersionCalc[VersionCalculatorService
version bumps] - DepGraph[DependencyGraphService
topological sort] - PkgUpdater[PackageUpdaterService
package.json updates] - NPM[NPMService
NPM registry] - - Helpers[Helper Utilities
commit attribution] - - Config --> Git - Config --> GitHub - Config --> Workspace - - Git --> Workspace - Workspace --> VersionCalc - Workspace --> DepGraph - - Git --> Helpers - Workspace --> Helpers - - Helpers --> VersionCalc - VersionCalc --> PkgUpdater - DepGraph --> PkgUpdater - Workspace --> PkgUpdater - - Workspace --> NPM - - GitHub -.-> |used by verify|Verify[verify.ts] - Git -.-> |used by verify|Verify - Workspace -.-> |used by verify|Verify - VersionCalc -.-> |used by verify|Verify - DepGraph -.-> |used by verify|Verify - Helpers -.-> |used by verify|Verify - - Git -.-> |used by prepare|Prepare[prepare flow] - Workspace -.-> |used by prepare|Prepare - VersionCalc -.-> |used by prepare|Prepare - DepGraph -.-> |used by prepare|Prepare - PkgUpdater -.-> |used by prepare|Prepare - Helpers -.-> |used by prepare|Prepare - - NPM -.-> |used by publish|Publish[publish flow
planned] - Git -.-> |used by publish|Publish - Workspace -.-> |used by publish|Publish - DepGraph -.-> |used by publish|Publish - - style Config fill:#e1f0ff - style Git fill:#ffe1e1 - style GitHub fill:#ffe1e1 - style Workspace fill:#e1ffe1 - style VersionCalc fill:#fff4e1 - style DepGraph fill:#fff4e1 - style PkgUpdater fill:#f0e1ff - style NPM fill:#ffe1e1 - style Helpers fill:#f0f0f0 - style Verify fill:#d4edda - style Prepare fill:#d4edda - style Publish fill:#e1f0ff,stroke:#4a90e2,stroke-dasharray: 5 5 +classDiagram + class ReleaseScriptsOptions { + +string repo + +string githubToken + +string workspaceRoot + +PackagesConfig packages + +BranchConfig branch + +GlobalCommitMode globalCommitMode + +boolean dryRun + } + + class ReleaseScriptsAPI { + +verify() Promise~void~ + +prepare() Promise~void~ + +publish() Promise~void~ + +packages.list() Promise~Package[]~ + +packages.get(name) Promise~Package~ + } + + class GitService { + <> + +create(branch) Effect~void~ + +checkout(branch) Effect~void~ + +getCommitsSince(ref) Effect~Commit[]~ + +getMostRecentPackageTag(pkg) Effect~string~ + +readFile(path, ref) Effect~string~ + +stage(files) Effect~void~ + +write(message) Effect~void~ + +push(branch) Effect~void~ + } + + class GitHubService { + <> + +getPullRequestByBranch(branch) Effect~PR | null~ + +createPullRequest(data) Effect~PR~ + +updatePullRequest(number, data) Effect~PR~ + +setCommitStatus(sha, state, description) Effect~void~ + } + + class WorkspaceService { + <> + +discoverPackages() Effect~Package[]~ + +readPackageJson(path) Effect~PackageJson~ + +writePackageJson(path, data) Effect~void~ + +getWorkspaceDependencies(pkg) Effect~string[]~ + } + + class VersionCalculatorService { + <> + +calculateBump(commits) Effect~BumpType~ + +applyBump(version, bump) Effect~string~ + +calculateVersions(packages) Effect~Map~ + } + + class DependencyGraphService { + <> + +buildGraph(packages) Effect~Graph~ + +topologicalSort(graph) Effect~Package[]~ + +detectCycles(graph) Effect~void~ + +assignLevels(sorted) Effect~Package[]~ + } + + class PackageUpdaterService { + <> + +applyRelease(pkg, version, all) Effect~void~ + +updateDependencies(pkg, all) Effect~PackageJson~ + +validateRanges(pkg) Effect~void~ + } + + class NPMService { + <> + +getPackument(name) Effect~Packument~ + +versionExists(name, version) Effect~boolean~ + +getLatestVersion(name) Effect~string~ + } + + class HelperUtilities { + <> + +loadOverrides() Effect~Overrides~ + +mergePackageCommitsIntoPackages() Effect~Package[]~ + +mergeCommitsAffectingGloballyIntoPackage() Effect~Package[]~ + +isGlobalCommit(commit, packages) boolean + +isDependencyFile(path) boolean + +findCommitRange(packages) CommitRange + } + + %% Configuration flows into services + ReleaseScriptsOptions ..> GitService : configures + ReleaseScriptsOptions ..> GitHubService : configures + ReleaseScriptsOptions ..> WorkspaceService : configures + + %% Service dependencies + WorkspaceService --> GitService : uses for file ops + VersionCalculatorService --> WorkspaceService : reads packages + DependencyGraphService --> WorkspaceService : analyzes packages + PackageUpdaterService --> WorkspaceService : updates packages + PackageUpdaterService --> VersionCalculatorService : applies versions + PackageUpdaterService --> DependencyGraphService : uses topo order + + %% Helper utilities + HelperUtilities --> GitService : reads overrides + HelperUtilities --> WorkspaceService : enriches packages + VersionCalculatorService --> HelperUtilities : uses for commits + + %% API uses services + ReleaseScriptsAPI --> GitService : git operations + ReleaseScriptsAPI --> GitHubService : PR & status + ReleaseScriptsAPI --> WorkspaceService : package discovery + ReleaseScriptsAPI --> VersionCalculatorService : version bumps + ReleaseScriptsAPI --> DependencyGraphService : dependency ordering + ReleaseScriptsAPI --> PackageUpdaterService : apply updates + ReleaseScriptsAPI --> NPMService : check/publish + ReleaseScriptsAPI --> HelperUtilities : helper functions + + %% Styling + class GitService,GitHubService,NPMService { + fill:#ffe1e1 + } + class WorkspaceService { + fill:#e1ffe1 + } + class VersionCalculatorService,DependencyGraphService { + fill:#fff4e1 + } + class PackageUpdaterService { + fill:#f0e1ff + } + class HelperUtilities { + fill:#f0f0f0 + } + class ReleaseScriptsOptions { + fill:#e1f0ff + } + class ReleaseScriptsAPI { + fill:#d4edda + } diff --git a/docs/diagrams/verify-workflow.mmd b/docs/diagrams/verify-workflow.mmd index 067c02f..e45f375 100644 --- a/docs/diagrams/verify-workflow.mmd +++ b/docs/diagrams/verify-workflow.mmd @@ -1,53 +1,108 @@ -flowchart TD - Start([Start verify]) --> FetchPR[Fetch Release PR by branch] - FetchPR --> CheckPR{PR exists?} - CheckPR -->|No| ErrorNoPR[Error: No release PR found] - CheckPR -->|Yes| LoadOverrides[Load version overrides
.github/ucdjs-release.overrides.json] +sequenceDiagram + actor User + participant API as verify() + participant GH as GitHubService + participant WS as WorkspaceService + participant Git as GitService + participant Helpers as Helper Utils + participant VC as VersionCalculator + participant DG as DependencyGraph - LoadOverrides --> DiscoverPkgs[Discover workspace packages
pnpm -r ls --json] - DiscoverPkgs --> FilterPkgs[Filter packages
include/exclude/private] + User->>API: Start verification - FilterPkgs --> GetTags[Get most recent tag
for each package] - GetTags --> FetchCommits[Fetch commits since tag
for each package] + Note over API,GH: 1. Fetch Release PR + API->>GH: getPullRequestByBranch(releaseBranch) + alt PR not found + GH-->>API: null + API-->>User: Error: No release PR found (exit 1) + else PR exists + GH-->>API: PR{number, title, head.sha} + end - FetchCommits --> MergeCommits[Merge package-specific commits] - MergeCommits --> GlobalCommits{Global commit
mode?} + Note over API,Helpers: 2. Load Configuration + API->>Helpers: loadOverrides() + Helpers->>Git: readFile(".github/ucdjs-release.overrides.json") + Git-->>Helpers: overrides JSON + Helpers-->>API: overrides{} - GlobalCommits -->|none| CalcVersions[Calculate expected versions
from commits] - GlobalCommits -->|all| MergeGlobalAll[Merge all global commits
into all packages] - GlobalCommits -->|dependencies| MergeGlobalDeps[Merge dependency-related
global commits] + Note over API,WS: 3. Discover Packages + API->>WS: discoverPackages() + WS->>WS: pnpm -r ls --json + WS->>WS: Filter by include/exclude/private + WS-->>API: packages[] - MergeGlobalAll --> CalcVersions - MergeGlobalDeps --> CalcVersions + Note over API,Git: 4. Fetch Commits for Each Package + loop For each package + API->>Git: getMostRecentPackageTag(pkg) + Git-->>API: tag (e.g., @pkg/name@1.0.0) + API->>Git: getCommitsSince(tag) + Git-->>API: commits[] + API->>API: Filter commits by package path + end - CalcVersions --> ApplyOverrides[Apply version overrides
from config] - ApplyOverrides --> BuildDepGraph[Build dependency graph] - BuildDepGraph --> CheckCircular{Circular
dependencies?} + Note over API,Helpers: 5. Merge Global Commits + alt globalCommitMode = "none" + API->>API: Skip global commits + else globalCommitMode = "all" + API->>Helpers: mergeCommitsAffectingGloballyIntoPackage(packages, "all") + Helpers->>Git: getCommitsSince(oldestTag) + Git-->>Helpers: all commits + Helpers->>Helpers: Filter global commits + Helpers->>Helpers: Timestamp-based attribution + Helpers-->>API: packages with global commits + else globalCommitMode = "dependencies" + API->>Helpers: mergeCommitsAffectingGloballyIntoPackage(packages, "dependencies") + Helpers->>Git: getCommitsSince(oldestTag) + Git-->>Helpers: all commits + Helpers->>Helpers: Filter dependency-related commits + Helpers->>Helpers: Timestamp-based attribution + Helpers-->>API: packages with dependency commits + end - CheckCircular -->|Yes| ErrorCircular[Error: Circular dependency] - CheckCircular -->|No| TopoSort[Compute topological order] + Note over API,VC: 6. Calculate Expected Versions + loop For each package + API->>VC: calculateBump(package.commits) + VC->>VC: Analyze conventional commits + VC-->>API: bumpType (major/minor/patch/none) + alt Has override + API->>API: Use override version + else No override + API->>VC: applyBump(currentVersion, bumpType) + VC-->>API: newVersion + end + end - TopoSort --> ReadActual[Read package.json files
from release branch HEAD] - ReadActual --> Compare[Compare expected vs actual
versions & dependencies] + Note over API,DG: 7. Build Dependency Graph + API->>DG: buildGraph(packages) + DG->>DG: Analyze workspace dependencies + alt Circular dependency + DG-->>API: CircularDependencyError + API-->>User: Error: Circular dependency (exit 1) + else No cycles + DG-->>API: graph + API->>DG: topologicalSort(graph) + DG-->>API: sortedPackages with levels + end - Compare --> CheckDrift{Drift detected?} - CheckDrift -->|Yes| ReportDrift[Report drift details
versions/dependencies] - CheckDrift -->|No| ReportSuccess[Report: All packages match] + Note over API,Git: 8. Read Actual Versions from Release Branch + loop For each package + API->>Git: readFile(package.json, releaseBranch) + Git-->>API: package.json content + API->>API: Parse actual version & dependencies + end - ReportDrift --> SetStatusFail[Set GitHub commit status
state: failure] - ReportSuccess --> SetStatusSuccess[Set GitHub commit status
state: success] + Note over API: 9. Compare Expected vs Actual + API->>API: Compare versions + API->>API: Compare dependency ranges - SetStatusFail --> Exit1([Exit code 1]) - SetStatusSuccess --> Exit0([Exit code 0]) - - ErrorNoPR --> Exit1 - ErrorCircular --> Exit1 - - style Start fill:#e1f5e1 - style Exit0 fill:#e1f5e1 - style Exit1 fill:#ffe1e1 - style ErrorNoPR fill:#ffcccc - style ErrorCircular fill:#ffcccc - style CheckDrift fill:#fff4e1 - style SetStatusSuccess fill:#d4edda - style SetStatusFail fill:#f8d7da + alt Drift detected + API->>API: Collect drift details + Note over API: Version mismatches or
dependency range issues + API->>GH: setCommitStatus(pr.head.sha, "failure", drift) + GH-->>API: Status updated + API-->>User: Drift detected (exit 1) + else No drift + API->>GH: setCommitStatus(pr.head.sha, "success") + GH-->>API: Status updated + API-->>User: All packages match (exit 0) + end From 72e87d87e0504ad9955af46113a8ece4b5b1afc2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 2 Jan 2026 14:22:43 +0100 Subject: [PATCH 4/4] docs: streamline styling in service diagrams --- docs/diagrams/service-dependency-graph.mmd | 31 +++++++--------------- docs/diagrams/version-bump-calculation.mmd | 23 +++++++++------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/diagrams/service-dependency-graph.mmd b/docs/diagrams/service-dependency-graph.mmd index 3b917a2..2b22a45 100644 --- a/docs/diagrams/service-dependency-graph.mmd +++ b/docs/diagrams/service-dependency-graph.mmd @@ -113,24 +113,13 @@ classDiagram ReleaseScriptsAPI --> HelperUtilities : helper functions %% Styling - class GitService,GitHubService,NPMService { - fill:#ffe1e1 - } - class WorkspaceService { - fill:#e1ffe1 - } - class VersionCalculatorService,DependencyGraphService { - fill:#fff4e1 - } - class PackageUpdaterService { - fill:#f0e1ff - } - class HelperUtilities { - fill:#f0f0f0 - } - class ReleaseScriptsOptions { - fill:#e1f0ff - } - class ReleaseScriptsAPI { - fill:#d4edda - } + style GitService fill:#ffe1e1 + style GitHubService fill:#ffe1e1 + style NPMService fill:#ffe1e1 + style WorkspaceService fill:#e1ffe1 + style VersionCalculatorService fill:#fff4e1 + style DependencyGraphService fill:#fff4e1 + style PackageUpdaterService fill:#f0e1ff + style HelperUtilities fill:#f0f0f0 + style ReleaseScriptsOptions fill:#e1f0ff + style ReleaseScriptsAPI fill:#d4edda diff --git a/docs/diagrams/version-bump-calculation.mmd b/docs/diagrams/version-bump-calculation.mmd index 3aa5ced..a8e10f9 100644 --- a/docs/diagrams/version-bump-calculation.mmd +++ b/docs/diagrams/version-bump-calculation.mmd @@ -2,31 +2,33 @@ flowchart TD Start([Package with commits]) --> HasOverride{Has version
override?} HasOverride -->|Yes| UseOverride[Use overridden version] - HasOverride -->|No| CheckCommits{Has commits?} + HasOverride -->|No| CheckCommits{Has commits
since last tag?} CheckCommits -->|No| NoBump[Bump type: none] - CheckCommits -->|Yes| AnalyzeCommits[Analyze each commit] + CheckCommits -->|Yes| InitLoop[Initialize: maxBump = none] - AnalyzeCommits --> LoopCommits{More commits?} + InitLoop --> LoopCommits{More commits
to check?} LoopCommits -->|Yes| NextCommit[Get next commit] NextCommit --> CheckBreaking{Breaking change?
BREAKING CHANGE:
or !} - CheckBreaking -->|Yes| Major[Bump type: major] + CheckBreaking -->|Yes| Major[Found: major bump] CheckBreaking -->|No| CheckFeat{Feature?
feat:} - CheckFeat -->|Yes| Minor[Bump type: minor] + CheckFeat -->|Yes| Minor[Found: minor bump] CheckFeat -->|No| CheckFix{Fix or perf?
fix: or perf:} - CheckFix -->|Yes| Patch[Bump type: patch] - CheckFix -->|No| Other[No bump for this commit] + CheckFix -->|Yes| Patch[Found: patch bump] + CheckFix -->|No| Other[Found: no bump] - Major --> UpdateMax[Update max bump type] + Major --> UpdateMax[Track highest bump
maxBump = max of current, found] Minor --> UpdateMax Patch --> UpdateMax Other --> LoopCommits UpdateMax --> LoopCommits - LoopCommits -->|No| ApplyBump[Apply highest bump type
to current version] + + LoopCommits -->|No| AllDone[All commits analyzed] + AllDone --> ApplyBump[Apply maxBump to current version] ApplyBump --> Result([New version]) UseOverride --> Result @@ -39,3 +41,6 @@ flowchart TD style Patch fill:#ccffcc style NoBump fill:#f0f0f0 style UseOverride fill:#e1f0ff + style InitLoop fill:#e6f3ff + style AllDone fill:#e6f3ff + style UpdateMax fill:#fff9e6