Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/rule-engine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Rule Engine

A TypeScript package for evaluating deployments against defined rules to
determine if they are allowed and which release should be chosen.

## Overview

The Rule Engine provides a framework for validating deployments based on
configurable rules. It follows a chain-of-responsibility pattern where rules are
applied sequentially to filter candidate releases.

## Core Components

- **RuleEngine**: The main class that sequentially applies rules to filter
candidate releases and selects the most appropriate one.
- **DeploymentResourceRule**: Interface that all rules must implement with a
`filter` method.
- **Releases**: Utility class for managing collections of releases.
- **DeploymentDenyRule**: Blocks deployments based on time restrictions using
recurrence rules.

## How It Works

1. Rule evaluation process:

- Starts with all available releases
- Applies each rule sequentially
- Updates the candidate list after each rule
- If any rule disqualifies all candidates, evaluation stops with denial
- After all rules pass, selects the final release

2. Release selection follows these priorities:

- Sequential upgrade releases get priority (oldest first)
- Specified desired release if available
- Otherwise, newest release by creation date

3. Tracking rejection reasons:
- Each rule provides specific reasons for rejecting individual releases
- The engine tracks these reasons per release ID across all rules
- The final result includes a map of rejected release IDs to their rejection reasons
- This approach eliminates the need for a general reason field, providing more detailed feedback

## Usage

```typescript
// Create rule instances
const denyRule = new DeploymentDenyRule({
// Configure time restrictions
recurrence: {
freq: Frequency.WEEKLY,
byday: [DayOfWeek.SA, DayOfWeek.SU],
},
timezone: "America/New_York",
});

// Create the rule engine
const engine = new RuleEngine([denyRule]);

// Evaluate against releases
const result = engine.evaluate(availableReleases, context);

// Check result
if (result.allowed) {
// Use result.chosenRelease
} else {
// Examine specific release rejection reasons
if (result.rejectionReasons) {
for (const [releaseId, reason] of result.rejectionReasons.entries()) {
console.log(`Release ${releaseId} was rejected because: ${reason}`);
}
}
}
```

## Extending

Create new rules by implementing the `DeploymentResourceRule` interface:

```typescript
class MyCustomRule implements DeploymentResourceRule {
filter(context: DeploymentContext, releases: Releases): RuleResult {
// Track rejection reasons
const rejectionReasons = new Map<string, string>();

// Custom logic to filter releases
const filteredReleases = releases.filter(release => {
// Determine if release meets criteria
const meetsCondition = /* your condition logic */;

// Track rejection reasons for releases that don't meet criteria
if (!meetsCondition) {
rejectionReasons.set(release.id, "Failed custom condition check");
}

return meetsCondition;
});

return {
allowedReleases: new Releases(filteredReleases),
rejectionReasons // Map of release IDs to rejection reasons
};
}
}
```

Add custom rules to the engine to extend functionality while maintaining the
existing evaluation flow.
13 changes: 13 additions & 0 deletions packages/rule-engine/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import baseConfig, { requireJsSuffix } from "@ctrlplane/eslint-config/base";

/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ["dist/**"],
rules: {
"@typescript-eslint/require-await": "off",
},
},
...requireJsSuffix,
...baseConfig,
];
41 changes: 41 additions & 0 deletions packages/rule-engine/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@ctrlplane/rule-engine",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"license": "MIT",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest",
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@ctrlplane/db": "workspace:*",
"@ctrlplane/validators": "workspace:*",
"@date-fns/tz": "^1.2.0",
"date-fns": "^4.1.0",
"rrule": "^2.8.1",
"zod": "catalog:"
},
"devDependencies": {
"@ctrlplane/eslint-config": "workspace:*",
"@ctrlplane/prettier-config": "workspace:*",
"@ctrlplane/tsconfig": "workspace:*",
"@types/node": "catalog:node22",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:",
"vitest": "^2.1.9"
},
"prettier": "@ctrlplane/prettier-config"
}
4 changes: 4 additions & 0 deletions packages/rule-engine/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./types.js";
export * from "./releases.js";
export * from "./rule-engine.js";
export * from "./rules/index.js";
Loading
Loading