Skip to content
Open
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [ main, 'release/v*' ]
pull_request:
branches: [ main, 'release/v*' ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install dependencies
run: go mod tidy
- name: Run tests
run: make check-coverage
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Release

on:
push:
tags:
- 'v*.*.*'

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install dependencies
run: go mod tidy
- name: Run tests
run: make check-coverage
- name: Announce release
run: echo "Release ${{ github.ref_name }} is ready."
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Go build and test artifacts
bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
coverage.out

# IDE/editor files
.vscode/
.idea/
*.swp
*.swo
.DS_Store

# Dependency directories
vendor/

# Logs
*.log
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
COVERAGE_FILE=coverage.out
MIN_COVERAGE=80

.PHONY: test coverage check-coverage clean release

test:
go test ./...

coverage:
go test -coverprofile=$(COVERAGE_FILE) ./...
go tool cover -func=$(COVERAGE_FILE)

check-coverage:
go test -coverprofile=$(COVERAGE_FILE) ./...
go tool cover -func=$(COVERAGE_FILE)
@cov=$(shell go tool cover -func=$(COVERAGE_FILE) | awk '/^total:/ {print $$3+0}') ; \
if [ $$(echo "$$cov < $(MIN_COVERAGE)" | bc) -eq 1 ]; then \
echo "FAIL: coverage below $(MIN_COVERAGE)% ($$cov%)"; exit 1; \
Comment on lines +16 to +18
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check-coverage target depends on bc being installed (echo ... | bc). That tool isn’t available by default in some environments/CI images, which can make coverage checks fail unexpectedly. Consider doing the comparison in awk (or Go) to avoid external dependencies, or document bc as a prerequisite.

Copilot uses AI. Check for mistakes.
else \
echo "PASS: coverage is $$cov%"; \
fi
Comment on lines +14 to +21
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In check-coverage, cov=$(shell ...) is evaluated by make during recipe expansion, before the preceding go test -coverprofile=... line generates coverage.out. This can result in go tool cover running against a missing/stale file and makes the target unreliable. Compute cov using shell command substitution ($$(...)) inside the recipe after generating the coverage file.

Copilot uses AI. Check for mistakes.

clean:
rm -f $(COVERAGE_FILE)

release:
git tag v$(VERSION)
git push origin v$(VERSION)
Comment on lines +26 to +28
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make release will happily run with an empty VERSION, creating/pushing a v tag, and it always pushes directly to origin. Add guards to require VERSION to be set/valid (and ideally ensure a clean working tree), and consider using an annotated tag (git tag -a) to reduce accidental releases.

Copilot uses AI. Check for mistakes.
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,110 @@ You can use it to:
- Build **decoupled applications** that scale safely
- Model systems with **type safety and explicit intent**
- Experiment with **Clean**, **Hexagonal**, **DDD**, or **Message-Driven** styles

## Serialization

The `foundation` package provides transport-agnostic serialization via the `Serializable` interface:

```go
// Serializable defines a transport-agnostic interface for serialization.
type Serializable interface {
ToData() ([]byte, error)
FromData([]byte) error
}
```

A JSON implementation is provided:

```go
// JSONSerializable implements Serializable using JSON encoding.
type JSONSerializable struct {
Value interface{}
}
```

### Testing

Tests for the JSON implementation are in `json_serializable_test.go`.
Run all tests with:

```
make test
```

Show coverage:

```
make coverage
```

Check and enforce minimum coverage (default 80%):

```
make check-coverage
```

Automate release tagging:

```
make release VERSION=0.1.0
```

## Contributor Instructions

- Run all tests before submitting a PR:
```
make test
```
- Check and enforce code coverage:
```
make check-coverage
```
- Do not commit coverage.out or build artifacts; these are ignored via .gitignore.
- Use idiomatic Go formatting and linting.
- For new features or bug fixes, add or update tests as needed.

## 🚀 Release Automation

### Prerequisites
- [git-chglog](https://github.com/git-chglog/git-chglog) must be installed for changelog generation.

### Release Workflow

1. **Automate the release process:**
```bash
./scripts/release.sh # Real release flow
./scripts/release.sh --dry-run # Preview actions only (no changes made)
```
This script will:
- Ensure you are on the main branch and up to date
- Determine the next version automatically
- Generate and commit the changelog for the new version
- Create a branch named `release/v<version>` from main
- Push the release branch to the remote
- Open a PR to main with the changelog as the PR body (requires GitHub CLI)

2. **After PR review and merge:**
- Tag the main branch with the new version (e.g., `git tag vX.X.X && git push origin vX.X.X`)
- The release workflow will run automatically on the new tag

### Example
```bash
./scripts/release.sh # For a real release
./scripts/release.sh --dry-run # To preview the release process
```

### Guidelines
- Always run tests and check coverage before releasing:
```bash
make check-coverage
```
- The release branch and tag are pushed automatically.
- Update documentation and changelog as needed before running the release script.

### For Contributors
- Do not manually edit the changelog for releases; use the automated scripts.
- Submit PRs from feature branches (e.g., `feature/your-feature`).
- Releases are managed by maintainers using the scripts above.

---
17 changes: 17 additions & 0 deletions pkg/forging/foundation/serialization/json_serializable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package foundation
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package name is foundation even though the folder path is foundation/serialization. This makes the public API awkward to consume (import path ends in serialization but code is referenced as foundation.JSONSerializable). Align package name with the directory (e.g., package serialization) or move the code under a foundation directory that matches the package name.

Suggested change
package foundation
package serialization

Copilot uses AI. Check for mistakes.

import (
"encoding/json"
)

type JSONSerializable struct {
Value interface{}
}

func (j *JSONSerializable) ToData() ([]byte, error) {
return json.Marshal(j.Value)
}

func (j *JSONSerializable) FromData(data []byte) error {
return json.Unmarshal(data, &j.Value)
Comment on lines +5 to +16
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FromData unmarshals into &j.Value (a *interface{}), which causes encoding/json to replace Value with generic map[string]any/[]any instead of populating the concrete destination. This breaks the intended round-trip and will make the test’s *testStruct type assertion fail. Unmarshal into the concrete destination stored in j.Value (and require it to be a non-nil pointer), or change the type to a generic JSONSerializable[T any] that holds *T so the destination type is explicit.

Suggested change
)
type JSONSerializable struct {
Value interface{}
}
func (j *JSONSerializable) ToData() ([]byte, error) {
return json.Marshal(j.Value)
}
func (j *JSONSerializable) FromData(data []byte) error {
return json.Unmarshal(data, &j.Value)
"fmt"
"reflect"
)
type JSONSerializable struct {
Value interface{}
}
func (j *JSONSerializable) ToData() ([]byte, error) {
return json.Marshal(j.Value)
}
func (j *JSONSerializable) FromData(data []byte) error {
if j == nil {
return fmt.Errorf("JSONSerializable receiver is nil")
}
if j.Value == nil {
return fmt.Errorf("JSONSerializable.Value must be a non-nil pointer; got <nil>")
}
v := reflect.ValueOf(j.Value)
if v.Kind() != reflect.Ptr || v.IsNil() {
return fmt.Errorf("JSONSerializable.Value must be a non-nil pointer; got %T", j.Value)
}
return json.Unmarshal(data, j.Value)

Copilot uses AI. Check for mistakes.
}
32 changes: 32 additions & 0 deletions pkg/forging/foundation/serialization/json_serializable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package foundation

import (
"testing"
)

type testStruct struct {
Name string
Age int
}

func TestJSONSerializable_ToDataAndFromData(t *testing.T) {
original := &JSONSerializable{Value: testStruct{Name: "Alice", Age: 30}}
data, err := original.ToData()
if err != nil {
t.Fatalf("ToData failed: %v", err)
}

copy := &JSONSerializable{Value: &testStruct{}}
err = copy.FromData(data)
if err != nil {
t.Fatalf("FromData failed: %v", err)
}

result, ok := copy.Value.(*testStruct)
if !ok {
t.Fatalf("Type assertion failed")
}
if result.Name != "Alice" || result.Age != 30 {
t.Errorf("Expected Alice, 30; got %s, %d", result.Name, result.Age)
}
Comment on lines +4 to +31
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file isn’t gofmt-formatted (spaces instead of tabs/standard formatting). Running gofmt (or ensuring the formatter runs in CI) will normalize indentation/import formatting and avoid noisy diffs later.

Suggested change
"testing"
)
type testStruct struct {
Name string
Age int
}
func TestJSONSerializable_ToDataAndFromData(t *testing.T) {
original := &JSONSerializable{Value: testStruct{Name: "Alice", Age: 30}}
data, err := original.ToData()
if err != nil {
t.Fatalf("ToData failed: %v", err)
}
copy := &JSONSerializable{Value: &testStruct{}}
err = copy.FromData(data)
if err != nil {
t.Fatalf("FromData failed: %v", err)
}
result, ok := copy.Value.(*testStruct)
if !ok {
t.Fatalf("Type assertion failed")
}
if result.Name != "Alice" || result.Age != 30 {
t.Errorf("Expected Alice, 30; got %s, %d", result.Name, result.Age)
}
"testing"
)
type testStruct struct {
Name string
Age int
}
func TestJSONSerializable_ToDataAndFromData(t *testing.T) {
original := &JSONSerializable{Value: testStruct{Name: "Alice", Age: 30}}
data, err := original.ToData()
if err != nil {
t.Fatalf("ToData failed: %v", err)
}
copy := &JSONSerializable{Value: &testStruct{}}
err = copy.FromData(data)
if err != nil {
t.Fatalf("FromData failed: %v", err)
}
result, ok := copy.Value.(*testStruct)
if !ok {
t.Fatalf("Type assertion failed")
}
if result.Name != "Alice" || result.Age != 30 {
t.Errorf("Expected Alice, 30; got %s, %d", result.Name, result.Age)
}

Copilot uses AI. Check for mistakes.
}
6 changes: 6 additions & 0 deletions pkg/forging/foundation/serialization/serializable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package serialization

type Serializable interface {
ToData() ([]byte, error)
FromData([]byte) error
}
19 changes: 19 additions & 0 deletions scripts/next_version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
set -e

# Get latest tag from remote
git fetch --tags
LATEST_TAG=$(git tag --sort=-v:refname | head -n1)

if [ -z "$LATEST_TAG" ]; then
echo "0.1.0"
exit 0
fi

# Parse version
IFS='.' read -r MAJOR MINOR PATCH <<< "${LATEST_TAG#v}"

# Bump patch version by default
PATCH=$((PATCH+1))
NEXT_VERSION="$MAJOR.$MINOR.$PATCH"
echo "$NEXT_VERSION"
74 changes: 74 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
#
# Automated release script for the Forging toolkit
#
# Usage:
# ./scripts/release.sh [--dry-run]
#
# - Must be run from the main branch after all implementation PRs are merged.
# - Determines the next version automatically.
# - Generates and commits the changelog.
# - Creates a release branch from main.
# - Pushes the release branch to origin.
# - Opens a PR to main with the changelog as the PR body (requires GitHub CLI).
# - In --dry-run mode, prints actions without making changes.
#
# Prerequisites:
# - git-chglog (https://github.com/git-chglog/git-chglog)
# - GitHub CLI (https://cli.github.com/)
#
# Example:
# ./scripts/release.sh # Real release flow
# ./scripts/release.sh --dry-run # Preview actions only
set -e

DRY_RUN=false
while [[ "$1" =~ ^- ]]; do
case $1 in
--dry-run) DRY_RUN=true ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
shift
done

# Check for required tools
gh_installed=$(command -v gh || true)
chglog_installed=$(command -v git-chglog || true)
if [ -z "$gh_installed" ]; then
echo "Error: GitHub CLI (gh) is required."
exit 1
fi
if [ -z "$chglog_installed" ]; then
echo "Error: git-chglog is required."
exit 1
fi

CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" != "main" ]; then
echo "Error: Release script must be run from the main branch."
exit 1
fi

echo "Syncing with remote..."
$DRY_RUN || git pull

VERSION=$(./scripts/next_version.sh)
BRANCH="release/v$VERSION"

echo "Creating release branch $BRANCH..."
$DRY_RUN || git checkout -b "$BRANCH"

echo "Generating changelog for v$VERSION..."
$DRY_RUN || git-chglog -o CHANGELOG.md "$VERSION"

$DRY_RUN || git add CHANGELOG.md
$DRY_RUN || git commit -m "chore: update CHANGELOG for v$VERSION"

$DRY_RUN || git push origin "$BRANCH"

CHANGELOG_BODY=$($DRY_RUN && echo "[DRY RUN]" || awk '/^## /{flag=1;next}/^$/{flag=0}flag' CHANGELOG.md | head -n -1)

echo "Opening PR to main..."
$DRY_RUN || gh pr create --base main --head "$BRANCH" --title "Release v$VERSION" --body "$CHANGELOG_BODY"

echo "Release branch and PR created for v$VERSION. Merge the PR to main, then tag the release."
Loading