diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..02304b1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=ours diff --git a/.gitea/HOWTO_RELEASE.md b/.gitea/HOWTO_RELEASE.md index cb05232..a37855d 100644 --- a/.gitea/HOWTO_RELEASE.md +++ b/.gitea/HOWTO_RELEASE.md @@ -122,3 +122,77 @@ git commit -m "chore(version): bump to 1.2.3" ``` > Nur die ersten beiden erscheinen im Changelog – der dritte wird **automatisch übersprungen**. + +--- + +## 🧾 Umgang mit `CHANGELOG.md` beim Mergen und Releasen + +Wenn du automatisiert einen Changelog mit `git-cliff` erzeugst, ist `CHANGELOG.md` ein **generiertes Artefakt** – und kein handgepflegter Quelltext. + +Beim Mergen von Feature-Branches in `main` kann es deshalb zu **unnötigen Konflikten** in dieser Datei kommen, obwohl der Inhalt später sowieso neu erzeugt wird. + +--- + +## 🧼 Umgang mit `CHANGELOG.md` in Feature-Branches + +Wenn du mit **Feature-Branches** arbeitest, wird `CHANGELOG.md` dort oft automatisch erzeugt. +Das kann beim späteren Merge in `main` zu **unnötigen Merge-Konflikten** führen. + +### ✅ Empfohlene Vorgehensweise + +**Bevor du den Branch mit `main` zusammenführst** (Merge oder Cherry-Pick): + +```bash +git rm CHANGELOG.md +git commit -m "chore(changelog): remove generated CHANGELOG.md before merge" +git push +``` + +Dadurch: + +* verhinderst du Merge-Konflikte mit `CHANGELOG.md` +* wird die Datei bei Feature-Branches nicht mehr automatisch erzeugt +* bleibt deine Historie sauber und konfliktfrei + +> 💡 Der Workflow erzeugt `CHANGELOG.md` automatisch **nur**, wenn: +> +> * die Datei schon vorhanden ist **oder** +> * der Branch `main` heißt + +--- + +## 🧩 Merge-Konflikte verhindern mit `.gitattributes` + +Damit Git bei Konflikten in `CHANGELOG.md` **automatisch deine Version bevorzugt**, kannst du folgende Zeile in die Datei `.gitattributes` aufnehmen: + +```gitattributes +CHANGELOG.md merge=ours +``` + +Das bedeutet: + +* Beim Merge wird die Version aus dem aktuellen Branch (`ours`) behalten +* Änderungen aus dem Ziel-Branch (`theirs`) werden verworfen + +### ✅ So verwendest du es richtig: + +1. **Füge die Regel in `main` hinzu**: + +```bash +echo "CHANGELOG.md merge=ours" >> .gitattributes +git add .gitattributes +git commit -m "chore(git): prevent merge conflicts in CHANGELOG.md" +git push origin main +``` + +2. **Hole sie in deinen Feature-Branch**: + +```bash +git checkout feature/xyz +git rebase origin/main +``` + +3. **Ab sofort werden Konflikte in `CHANGELOG.md` automatisch aufgelöst** – lokal. + +> ⚠️ Hinweis: Plattformen wie **Gitea, GitHub oder GitLab ignorieren `.gitattributes` beim Merge über die Web-Oberfläche**. +> Führe Merges daher **lokal** durch, wenn du Konflikte verhindern willst. diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0d55559..8f02b21 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -4,38 +4,44 @@ on: push: branches: - main + - '**' jobs: detect-version-change: runs-on: ubuntu-latest outputs: - version_changed: ${{ steps.check.outputs.version_changed }} + version_changed: ${{ steps.set.outputs.version_changed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check if VERSION file changed - id: check + if: github.ref == 'refs/heads/main' run: | echo "🔍 Vergleich mit github.event.before:" echo "Before: ${{ github.event.before }}" echo "After: ${{ github.sha }}" - + echo "📄 Changed files between before and after:" git diff --name-only ${{ github.event.before }} ${{ github.sha }} || echo "(diff failed)" if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q '^VERSION$'; then - echo "✅ VERSION file was changed between before and after" - echo "version_changed=true" >> $GITHUB_OUTPUT + echo "✅ VERSION file was changed" + echo "VERSION_CHANGED=true" >> $GITHUB_ENV else - echo "ℹ️ VERSION file not changed between before and after" - echo "version_changed=false" >> $GITHUB_OUTPUT + echo "ℹ️ VERSION file not changed" + echo "VERSION_CHANGED=false" >> $GITHUB_ENV fi + - name: Set output (always) + id: set + run: | + echo "version_changed=${VERSION_CHANGED:-false}" >> $GITHUB_OUTPUT + changelog-only: needs: detect-version-change - if: needs.detect-version-change.outputs.version_changed == 'false' + if: github.ref != 'refs/heads/main' || needs.detect-version-change.outputs.version_changed == 'false' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -65,8 +71,14 @@ jobs: run: | cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea - - name: Generate unreleased changelog - run: git-cliff -c cliff.toml -o CHANGELOG.md + - name: Generate unreleased changelog (if file exists or on main) + run: | + if [[ -f CHANGELOG.md || "${GITHUB_REF##refs/heads/}" == "main" ]]; then + echo "Generating CHANGELOG.md..." + git-cliff -c cliff.toml -o CHANGELOG.md + else + echo "CHANGELOG.md does not exist and this is not 'main'. Skipping generation." + fi - name: Commit updated CHANGELOG run: | @@ -75,12 +87,12 @@ jobs: echo "No changes to commit" else git commit -m "chore(changelog): update unreleased changelog" - git push origin main + git push origin "${GITHUB_REF##refs/heads/}" fi release: needs: detect-version-change - if: needs.detect-version-change.outputs.version_changed == 'true' + if: needs.detect-version-change.outputs.version_changed == 'true' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a97511..8be03b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,19 +6,30 @@ All notable changes to this project will be documented in this file. ### 🚀 Features +- *(workflows)* Conditionally generate changelog - ([2ab6f1b](https://git.0xmax42.io/maxp/http-kernel/commit/2ab6f1b8db2d7bd31ca30248d0de183f17a5738c)) +- *(interfaces)* Add pipeline executor interface - ([927a908](https://git.0xmax42.io/maxp/http-kernel/commit/927a9081d4f363202520d017eb424c7c097ced94)) +- *(pipeline)* Add configuration and hooks for pipeline execution - ([3f114bb](https://git.0xmax42.io/maxp/http-kernel/commit/3f114bb68d94c48a53514752d57cb4f01adeaae3)) - *(workflows)* Add CI for Deno project tests - ([9d5db4f](https://git.0xmax42.io/maxp/http-kernel/commit/9d5db4f414cf961248f2b879f2b132b81a32cb92)) +### 🐛 Bug Fixes + +- *(workflows)* Ensure version detection output is always set - ([abd2d6e](https://git.0xmax42.io/maxp/http-kernel/commit/abd2d6e8402662f863d9974aaa0bc228a4777724)) + ### 🚜 Refactor - *(workflows)* Rename changelog file for consistency - ([b9d25f2](https://git.0xmax42.io/maxp/http-kernel/commit/b9d25f23fc6ad7696deee319024aa5b1af4d98c0)) ### 📚 Documentation +- *(release)* Update guidelines for handling changelog - ([6ce73c1](https://git.0xmax42.io/maxp/http-kernel/commit/6ce73c14fa6736b622e646feb61522e6ec1f4c5a)) +- *(pipeline)* Add design plan for PipelineExecutor class - ([0846dbb](https://git.0xmax42.io/maxp/http-kernel/commit/0846dbb758ba788f969a381c56498920ee0f9562)) - Add README for HttpKernel project - ([a1ce306](https://git.0xmax42.io/maxp/http-kernel/commit/a1ce30627c68a3f869eb6a104308322af8596dc1)) - Add MIT license file - ([5118a19](https://git.0xmax42.io/maxp/http-kernel/commit/5118a19aeaa1102591aa7fe093fdec1aa19dc7f5)) ### ⚙️ Miscellaneous Tasks +- *(git)* Ignore merge conflicts for CHANGELOG.md - ([d04dfcd](https://git.0xmax42.io/maxp/http-kernel/commit/d04dfcd63ea2478ffdff2e966d310194dafd8d7d)) +- *(workflows)* Refine branch handling in release process - ([8f94cc9](https://git.0xmax42.io/maxp/http-kernel/commit/8f94cc915c75a11efa1a8e3bdc51ffea9c2f19b5)) - *(workflows)* Update changelog file extension to .md and revert b9d25f23fc - ([a88b4d1](https://git.0xmax42.io/maxp/http-kernel/commit/a88b4d112f5c07664d41f6e9d03246307551f25d)) - Rename changelog and readme files to use .md extension - ([4f2b650](https://git.0xmax42.io/maxp/http-kernel/commit/4f2b65049f461ef377e7231905fd066cbc3c7fe0)) - *(workflows)* Update test workflow for http-kernel project - ([0311546](https://git.0xmax42.io/maxp/http-kernel/commit/03115464e0fb01b8ca00a2fdabde013d004ae8a2)) diff --git a/docs/pipeline_executor_plan.md b/docs/pipeline_executor_plan.md new file mode 100644 index 0000000..8fb6b0a --- /dev/null +++ b/docs/pipeline_executor_plan.md @@ -0,0 +1,72 @@ +# 🧩 Plan: `PipelineExecutor` + +## 🎯 Ziel +Eine eigenständige, testbare Klasse zur Ausführung einer Middleware- und Handler-Pipeline, die: +- Linear und sauber das `next()`-Verhalten abbildet +- Typvalidierung durchführt (`isMiddleware`, `isHandler`) +- Fehler behandelt und an konfigurierbare Handler weiterleitet +- Optionale Hooks zur Tracing-Integration bietet (z. B. für Zeitmessung, Logging) +- Am Ende eine dekorierte `Response` zurückliefert + +--- + +## 🧩 Schnittstelle (API) + +```ts +class PipelineExecutor { + constructor(cfg: IHttpKernelConfig); + + run( + ctx: TContext, + middleware: Middleware[], + handler: Handler, + hooks?: IPipelineHooks, // optional + ): Promise; +} +``` + +--- + +## 🪝 Hook-Schnittstelle (`IPipelineHooks`) + +```ts +interface IPipelineHooks { + onPipelineStart?(ctx: TContext): void; + onStepStart?(name: string | undefined, ctx: TContext): void; + onStepEnd?(name: string | undefined, ctx: TContext, duration: number): void; + onPipelineEnd?(ctx: TContext, totalDuration: number): void; +} +``` + +- `name` ist `undefined`, wenn keine `.name` am Handler/Middleware gesetzt ist +- Diese Hooks ermöglichen später Logging, Zeitmessung, Statistiken etc. +- Der `TraceManager` wird dieses Interface implementieren + +--- + +## 🛠️ Interne Aufgaben / Ablauf + +1. `run(...)` beginnt mit Aufruf `onPipelineStart(ctx)` +2. Zeitmessung (`performance.now()`) +3. Dispatcher-Funktion führt jede Middleware mit `next()`-Kette aus +4. Vor jedem Aufruf: `onStepStart(name, ctx)` +5. Nach jedem Aufruf: `onStepEnd(name, ctx, duration)` +6. Nach letztem Handler: `onPipelineEnd(ctx, totalDuration)` +7. Ergebnis wird durch `cfg.decorateResponse(res, ctx)` geschickt +8. Im Fehlerfall: `cfg.httpErrorHandlers[500](ctx, error)` + +--- + +## ✅ Vorteile + +- `HttpKernel` ist von Ausführungsdetails entkoppelt +- Tracing-/Logging-System kann ohne Invasivität angeschlossen werden +- Sehr gut testbar (z. B. Middleware-Mock + Hook-Aufrufe prüfen) +- Erweiterbar für Timeout, Async-Context, Abbruchlogik etc. + +--- + +## 📦 Dateiname-Vorschlag + +- `src/Core/PipelineExecutor.ts` oder +- `src/HttpKernel/PipelineExecutor.ts` diff --git a/src/Interfaces/IPipelineExecutor.ts b/src/Interfaces/IPipelineExecutor.ts new file mode 100644 index 0000000..a25ce09 --- /dev/null +++ b/src/Interfaces/IPipelineExecutor.ts @@ -0,0 +1,50 @@ +import { Handler, Middleware } from '../Types/mod.ts'; +import { IContext } from './IContext.ts'; +import { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts'; + +/** + * Constructor type for a class implementing the IPipelineExecutor interface. + * + * This can be used for dependency injection, factory-based initialization, + * or dynamic instantiation of pipeline executors. + * + * @template TContext - The extended context type passed through the pipeline. + */ +export interface PipelineExecutorFactory { + /** + * Creates a new instance of a pipeline executor. + * + * @param config - Configuration used to control error handling, + * response decoration and lifecycle hooks. + */ + new ( + config: IPipelineExecutorConfig, + ): IPipelineExecutor; +} + +/** + * Defines the contract for executing a middleware and handler pipeline. + * + * The pipeline is responsible for: + * - Executing middleware in order with `next()` chaining + * - Invoking the final handler + * - Applying optional lifecycle hooks + * - Producing and decorating a Response + * + * @template TContext - The context type flowing through the pipeline. + */ +export interface IPipelineExecutor { + /** + * Executes the middleware pipeline and returns the final Response. + * + * @param ctx - The context object representing the current HTTP request state. + * @param middleware - An ordered array of middleware functions to be executed. + * @param handler - The final route handler to be called after all middleware. + * @returns A Promise resolving to the final HTTP Response. + */ + run( + ctx: TContext, + middleware: Middleware[], + handler: Handler, + ): Promise; +} diff --git a/src/Interfaces/IPipelineExecutorConfig.ts b/src/Interfaces/IPipelineExecutorConfig.ts new file mode 100644 index 0000000..616d716 --- /dev/null +++ b/src/Interfaces/IPipelineExecutorConfig.ts @@ -0,0 +1,33 @@ +import { ResponseDecorator } from '../Types/ResponseDecorator.ts'; +import { IContext } from './IContext.ts'; +import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; +import { IPipelineHooks } from './IPipelineHooks.ts'; + +/** + * Configuration object for the PipelineExecutor, defining how to handle + * errors, responses, and tracing hooks. + * + * This allows the execution logic to remain decoupled from kernel-level behavior + * while still supporting custom behavior injection. + * + * @template TContext - The context type propagated during pipeline execution. + */ +export interface IPipelineExecutorConfig { + /** + * Optional function used to transform or decorate the final Response object + * before it is returned to the client. + */ + decorateResponse?: ResponseDecorator; + + /** + * Optional map of error handlers, keyed by HTTP status codes (e.g., 404, 500). + * These handlers are invoked if an error occurs during middleware or handler execution. + */ + errorHandlers?: IHttpErrorHandlers; + + /** + * Optional hooks that allow tracing and lifecycle monitoring during pipeline execution. + * Each hook is called at a specific phase of the middleware/handler lifecycle. + */ + pipelineHooks?: IPipelineHooks; +} diff --git a/src/Interfaces/IPipelineHooks.ts b/src/Interfaces/IPipelineHooks.ts new file mode 100644 index 0000000..32a979a --- /dev/null +++ b/src/Interfaces/IPipelineHooks.ts @@ -0,0 +1,36 @@ +import { + OnPipelineEnd, + OnPipelineStart, + OnStepEnd, + OnStepStart, +} from '../Types/mod.ts'; +import { IContext } from './IContext.ts'; + +/** + * A set of optional hook functions that can be triggered during pipeline execution. + * These hooks allow tracing, performance measurement, and logging to be integrated + * without altering middleware or handler logic. + * + * @template TContext - The custom context type used within the application. + */ +export interface IPipelineHooks { + /** + * Triggered once before any middleware or handler is executed. + */ + onPipelineStart?: OnPipelineStart; + + /** + * Triggered immediately before each middleware or handler runs. + */ + onStepStart?: OnStepStart; + + /** + * Triggered immediately after each middleware or handler has finished executing. + */ + onStepEnd?: OnStepEnd; + + /** + * Triggered after the entire pipeline completes execution. + */ + onPipelineEnd?: OnPipelineEnd; +} diff --git a/src/Interfaces/mod.ts b/src/Interfaces/mod.ts index 7c235d9..ab31600 100644 --- a/src/Interfaces/mod.ts +++ b/src/Interfaces/mod.ts @@ -5,6 +5,12 @@ export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts'; export type { IHttpKernel } from './IHttpKernel.ts'; export type { IHttpKernelConfig } from './IHttpKernelConfig.ts'; export type { IInternalRoute } from './IInternalRoute.ts'; +export type { + IPipelineExecutor, + PipelineExecutorFactory, +} from './IPipelineExecutor.ts'; +export type { IPipelineExecutorConfig } from './IPipelineExecutorConfig.ts'; +export type { IPipelineHooks } from './IPipelineHooks.ts'; export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts'; export { isDynamicRouteDefinition, diff --git a/src/Types/PipelineHooks.ts b/src/Types/PipelineHooks.ts new file mode 100644 index 0000000..f1b0ce1 --- /dev/null +++ b/src/Types/PipelineHooks.ts @@ -0,0 +1,49 @@ +import { IContext } from '../Interfaces/mod.ts'; + +/** + * A callback invoked when the middleware pipeline starts. + * + * @template TContext - The context type passed throughout the pipeline. + * @param ctx - The context object for the current request. + */ +export type OnPipelineStart = ( + ctx: TContext, +) => void; + +/** + * A callback invoked immediately before a middleware or handler is executed. + * + * @template TContext - The context type passed throughout the pipeline. + * @param name - Optional name of the current middleware or handler, if defined. + * @param ctx - The context object for the current request. + */ +export type OnStepStart = ( + name: string | undefined, + ctx: TContext, +) => void; + +/** + * A callback invoked immediately after a middleware or handler has completed. + * + * @template TContext - The context type passed throughout the pipeline. + * @param name - Optional name of the current middleware or handler, if defined. + * @param ctx - The context object for the current request. + * @param duration - Execution time in milliseconds. + */ +export type OnStepEnd = ( + name: string | undefined, + ctx: TContext, + duration: number, +) => void; + +/** + * A callback invoked after the entire pipeline has completed execution. + * + * @template TContext - The context type passed throughout the pipeline. + * @param ctx - The context object for the current request. + * @param totalDuration - Total execution time of the pipeline in milliseconds. + */ +export type OnPipelineEnd = ( + ctx: TContext, + totalDuration: number, +) => void; diff --git a/src/Types/mod.ts b/src/Types/mod.ts index c159495..98da911 100644 --- a/src/Types/mod.ts +++ b/src/Types/mod.ts @@ -39,6 +39,12 @@ export type { HttpStatusCode } from './HttpStatusCode.ts'; export { isMiddleware } from './Middleware.ts'; export type { Middleware } from './Middleware.ts'; export type { Params } from './Params.ts'; +export type { + OnPipelineEnd, + OnPipelineStart, + OnStepEnd, + OnStepStart, +} from './PipelineHooks.ts'; export type { Query } from './Query.ts'; export type { RegisterRoute } from './RegisterRoute.ts'; export type { ResponseDecorator } from './ResponseDecorator.ts';