release: v0.4.8

Introduce the project versioning system (MAJOR.MINOR.PATCH, each 0-999)
and tag the first release.

- VERSION file as single source of truth (0.4.8)
- __manifest__.py version -> 17.0.0.4.8 (odoo series + product version)
- CHANGELOG.md with plain-language v0.4.8 release notes
- scripts/bump-version.sh: bump (major/minor/patch/set) + tag from CHANGELOG
- README.md: version header, 69-test status, changelog link
- CLAUDE.md §15 Versioning & Releases; corrected Odoo 17 mail.template /
  blog.post.content compatibility table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nicholas Riegel 2026-05-30 03:27:18 -04:00
parent f5169c1a81
commit b4d1e577df
6 changed files with 276 additions and 13 deletions

46
CHANGELOG.md Normal file
View file

@ -0,0 +1,46 @@
# Changelog
All notable changes to **ITSulu Blog Publisher** are recorded here.
This project uses a three-part version number — `MAJOR.MINOR.PATCH` (each part 0999):
- **MAJOR** — a major release for sale; significant feature upgrades or a significant change to the software.
- **MINOR** — one or more features added, or a meaningful performance improvement.
- **PATCH** — a single group of commits, or one large commit.
Release notes are written in plain language so anyone on the team can follow what changed.
---
## v0.4.8 — 2026-05-30
The first tagged release. This version gets the whole test suite running green on the
ITSulu Kubernetes cluster and fixes two bugs that affected real blog posts and emails.
### Fixed
- **Notification emails now render correctly.** The email template was written in an old
syntax that Odoo 17 no longer understands, so notification emails were going out with raw
code (like `${object.blog_post_id.name}`) instead of the actual post title. The template
was rebuilt using Odoo 17's current format, so subjects and bodies now fill in properly.
- **Generated blog posts are no longer empty.** When a post was created automatically, the
AI-written body was being thrown away before it reached the post. Every auto-generated post
was published blank. The body is now saved to the post's content field.
- Removed a duplicate field definition that produced a warning on every Odoo startup.
### Testing & Infrastructure
- The full automated test suite — **69 tests** (48 unit, 15 behaviour, 6 performance) — now
passes end to end on the ITSulu Kubernetes test cluster.
- The CI/CD pipeline was corrected: stages now run in the right order, database credentials
are handled properly, and test reports are generated as build artifacts.
- End-to-end tests now run as ephemeral jobs on the ITSulu cluster (`itsulu-testing`
namespace) instead of an external preview service.
- Brought the test code up to date with Odoo 17 (email rendering, blog post fields, and the
behaviour-test environment setup).
### Documentation
- Recorded the Odoo 16 → 17 migration lessons (email templates, post body fields, template
database refresh) in `CLAUDE.md` so they are not rediscovered the hard way.
- Introduced this changelog and the project versioning scheme.

View file

@ -797,10 +797,13 @@ When extending core Odoo models (e.g., `blog.post`), be aware of field differenc
| Model | Field | Odoo 16 | Odoo 17 | Note |
|-------|-------|---------|---------|------|
| `blog.post` | `body_arch` | Yes | **No** | XML layout field; removed in Odoo 17 |
| `blog.post` | `body` | Yes | **No** | HTML body field; use editor interface instead |
| `blog.post` | `body_arch` | Yes | **No** | Removed in Odoo 17 |
| `blog.post` | `body` | Yes | **No** | Removed in Odoo 17 |
| `blog.post` | `content` | — | **Yes** | The HTML body field in Odoo 17 — **writable**; auto-generation MUST write `'content': llm_response.body_html` or posts publish empty |
| `blog.post` | `itsulu_social_id` | N/A | Custom | Add via `_inherit` for reverse relationship to custom models |
| `mail.template` | Subject/Body | Mako syntax | Mako syntax | Renders async in send_mail(); use Mako `% for` loops, not method calls in templates |
| `mail.template` | subject / email_from | Mako `${}` | **inline_template `{{ }}`** | Mako `${}` is dead in Odoo 17 — renders literally |
| `mail.template` | body_html | Mako `${}`/`% for` | **qweb `type="html"`** | Use `<t t-out="">`, `<t t-foreach t-as>`, `<t t-if>` as real XML children. NOT `type="qweb"` (invalid RNG type) |
| `mail.template` | render method | `generate_email()` | **`_render_field(field, [ids])`** | Returns `{id: rendered_str}`; `_generate_template` returns UNrendered text |
**Pattern for extending blog.post safely:**
```python
@ -817,15 +820,24 @@ class BlogPost(models.Model):
**Pattern for creating blog.post test records:**
```python
# Use sudo() to bypass Odoo validation when creating minimal test posts
# Use sudo() to bypass Odoo validation when creating minimal test posts.
# The body field in Odoo 17 is `content` (NOT body / body_arch / body_html).
post = self.env['blog.post'].sudo().create({
'name': 'Test Post',
'blog_id': blog.id,
'is_published': True,
# Do NOT include body, body_arch, or body_html — use website editing UI
'content': '<p>HTML body goes here</p>', # Odoo 17 body field — writable
})
```
**Pattern for asserting on a rendered mail.template (synchronous):**
```python
template = self.env.ref('module.email_template_xmlid')
rendered = template._render_field('subject', [record.id]) # {id: 'rendered text'}
assert 'Expected' in rendered[record.id]
# body_html: template._render_field('body_html', [record.id])
```
---
## 12. Failure Recovery Quick Reference
@ -915,7 +927,79 @@ assert 800 <= log.tokens_used <= 1200
---
## 14. Phased Adoption Triggers
## 15. Versioning & Releases
The project uses a three-part version number, **`MAJOR.MINOR.PATCH`**, each part `0999`.
| Part | Bump when… | Examples |
|---|---|---|
| **MAJOR** | A major release for sale: significant feature upgrade, or a significant change to the software. | new product tier, rewrite, breaking redesign |
| **MINOR** | One or more features added, or a meaningful performance improvement. | new LLM provider, new wizard, 2× faster generation |
| **PATCH** | A single group of commits, or one large commit. | bug-fix batch, doc pass, refactor, dependency bump |
### Sources of truth & where the version lives
The repo-root **`VERSION`** file is the single source of truth. These are kept in sync:
| File | Form | Notes |
|---|---|---|
| `VERSION` | `0.4.8` | Source of truth — plain text, one line |
| `addons/itsulu_blog_publisher/__manifest__.py` | `'version': '17.0.0.4.8'` | Odoo manifest = `<odoo_series>.<product_version>` = `17.0` + `0.4.8` |
| `README.md` | `**Version:** 0.4.8` | Header line + status footer |
| `CHANGELOG.md` | `## v0.4.8 — <date>` | Release notes section per version |
| git tag | `v0.4.8` | Annotated tag; message = the CHANGELOG section |
**Never hand-edit the version in multiple files.** Use the helper script — it updates all of
them from `VERSION` and derives the Odoo manifest version (`17.0.<MAJOR>.<MINOR>.<PATCH>`).
### Release process (every version change)
```bash
# 1. Bump the version (updates VERSION, manifest, README)
scripts/bump-version.sh patch # or: minor | major | set X.Y.Z
# 2. Write the release notes — add a "## vX.Y.Z — <date>" section to CHANGELOG.md
# in plain, common language (what changed, why it matters), NOT git-speak.
# Group under Fixed / Added / Changed / Testing & Infrastructure / Documentation.
# 3. Commit the bump + notes together
git add -A && git commit -m "release: vX.Y.Z"
# 4. Tag the release — the tag message is taken from the CHANGELOG section
scripts/bump-version.sh tag
# 5. Push branch and tag
git push && git push origin vX.Y.Z
```
### Release-notes style (common language)
Write for a teammate, not a compiler. Each entry says **what changed and why it matters** in
one or two sentences of plain English. Prefer:
> - **Notification emails now render correctly.** They were going out with raw code in the
> subject instead of the post title; the template was rebuilt for Odoo 17.
over:
> - fix: migrate mail.template body_html ${} → qweb t-out
Keep the technical detail for commit messages and the CLAUDE.md failure table; the CHANGELOG is
the human-readable record. Group entries under **Fixed / Added / Changed / Testing &
Infrastructure / Documentation** as applicable.
### Rules
- One tag per version; tags are immutable. If a release is wrong, ship the next PATCH — never
move a tag.
- Tag on the release branch **after** the work is merged/finalised, so the tag points at the
commit that actually ships.
- The Odoo manifest version must always be `17.0.` + the `VERSION` value. The `-u` upgrade path
relies on the manifest version increasing, so bump before deploying schema/data changes.
---
## 16. Phased Adoption Triggers
**Add Phase 2 (BDD, factories, coverage gates) when ≥ 2:**
- Stakeholders ask "how do we know X works?"

View file

@ -1,7 +1,11 @@
# ITSulu Blog Publisher — Odoo 17 Addon
**Version:** 0.4.8
Automated blog post generation and publishing for Odoo 17 Community Edition, powered by generative AI (Anthropic Claude, OpenAI, Google Gemini, or Ollama).
> **Versioning**`MAJOR.MINOR.PATCH` (each 0999). MAJOR = major release for sale / significant change; MINOR = features or performance improvements; PATCH = a single group of commits. See [CHANGELOG.md](CHANGELOG.md) for release notes and [CLAUDE.md](CLAUDE.md) §15 for the full scheme.
## Features
### ✨ Core Functionality
@ -28,13 +32,14 @@ Automated blog post generation and publishing for Odoo 17 Community Edition, pow
### 📊 Test Coverage
**Phase 2 Complete**: 63 automated tests across 6 test files
**69 automated tests**, all passing on the ITSulu Kubernetes test cluster.
| Test Suite | Count | Status |
|---|---|---|
| Unit Tests (TDD) | 48 | ✅ All Passing |
| Behavior Tests (BDD) | 15 | ✅ All Passing |
| **Total** | **63** | **100% ✅** |
| Behaviour Tests (BDD) | 15 | ✅ All Passing |
| Performance Benchmarks | 6 | ✅ All Passing |
| **Total** | **69** | **100% ✅** |
**Coverage Areas**:
- Topic queue management (7 tests)
@ -42,7 +47,11 @@ Automated blog post generation and publishing for Odoo 17 Community Edition, pow
- Social media copy (16 tests)
- Schedule slots (10 tests)
- LLM router dispatch (7 tests)
- E2E workflows (15 BDD scenarios)
- E2E / behaviour workflows (15 BDD scenarios)
- Performance SLOs — latency, query count, token usage, concurrency (6 benchmarks)
Tests run as ephemeral Kubernetes jobs in the `itsulu-testing` namespace against a
primed PostgreSQL template database. See [CLAUDE.md](CLAUDE.md) §8 for the K8s test setup.
## Installation
@ -232,7 +241,8 @@ pre-commit run --all-files
## Documentation
- [CLAUDE.md](CLAUDE.md) — Complete TDD framework, testing patterns, troubleshooting
- [CHANGELOG.md](CHANGELOG.md) — Release notes and version history
- [CLAUDE.md](CLAUDE.md) — Complete TDD framework, testing patterns, versioning scheme, troubleshooting
- [ARCHITECTURE.md](ARCHITECTURE.md) — System design, data flow, API contracts
- [PHASE2_ROADMAP.md](PHASE2_ROADMAP.md) — Phase 2 implementation status
@ -260,6 +270,6 @@ https://gitlab.com/itsulu-odoo/itsulu-blog-publisher/issues
---
**Current Status**: Phase 2 Complete ✅ (63/63 tests passing)
**Current Status**: v0.4.8 — test suite green (69/69 passing on ITSulu K8s) ✅
**Last Updated**: 2026-05-30
**Maintainer**: Nicholas Riegel (nicholasr@itsulu.com)

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.4.8

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
{
'name': 'ITSulu Blog Publisher',
'version': '17.0.1.0.0',
# Odoo manifest version = <odoo_series>.<product_version>. Product version
# is tracked in the repo-root VERSION file (currently 0.4.8). See CLAUDE.md §15.
'version': '17.0.0.4.8',
'summary': 'AI-powered blog post generation with multi-LLM support, scheduling, and social media copy',
'description': """
ITSulu Blog Publisher

120
scripts/bump-version.sh Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
#
# bump-version.sh — ITSulu Blog Publisher version manager
#
# Versioning scheme: MAJOR.MINOR.PATCH (each part 0999)
# MAJOR major release for sale; significant feature upgrade / significant change
# MINOR feature(s) added, or a performance improvement
# PATCH a single group of commits, or one large commit
#
# The repo-root VERSION file is the single source of truth. The Odoo manifest
# version is derived as 17.0.<MAJOR>.<MINOR>.<PATCH> (Odoo-series prefix +
# the product version), e.g. product 0.4.8 -> manifest 17.0.0.4.8.
#
# Usage:
# scripts/bump-version.sh patch # 0.4.8 -> 0.4.9
# scripts/bump-version.sh minor # 0.4.8 -> 0.5.0
# scripts/bump-version.sh major # 0.4.8 -> 1.0.0
# scripts/bump-version.sh set 1.2.3 # set an explicit version
# scripts/bump-version.sh tag # create annotated git tag v<VERSION>
# # from the CHANGELOG section
#
# Typical flow for a release:
# 1) scripts/bump-version.sh minor
# 2) edit CHANGELOG.md — fill in the new "## vX.Y.Z" section (plain language)
# 3) git add -A && git commit -m "release: vX.Y.Z"
# 4) scripts/bump-version.sh tag # tags + uses the CHANGELOG notes
# 5) git push && git push --tags
#
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION_FILE="$ROOT/VERSION"
MANIFEST="$ROOT/addons/itsulu_blog_publisher/__manifest__.py"
README="$ROOT/README.md"
CHANGELOG="$ROOT/CHANGELOG.md"
ODOO_SERIES="17.0"
die() { echo "error: $*" >&2; exit 1; }
read_version() {
[ -f "$VERSION_FILE" ] || die "VERSION file not found at $VERSION_FILE"
tr -d '[:space:]' < "$VERSION_FILE"
}
validate() {
local v="$1"
[[ "$v" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] \
|| die "invalid version '$v' — expected MAJOR.MINOR.PATCH, each part 0999"
for part in "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"; do
[ "$part" -le 999 ] || die "version part '$part' exceeds 999"
done
}
write_everywhere() {
local v="$1"
echo "$v" > "$VERSION_FILE"
# Odoo manifest: 17.0.<MAJOR>.<MINOR>.<PATCH> (series prefix + product version)
sed -i -E "s/('version': ')[0-9.]+(')/\1${ODOO_SERIES}.${v}\2/" "$MANIFEST"
# README version line: **Version:** X.Y.Z
if grep -qE '^\*\*Version:\*\*' "$README"; then
sed -i -E "s/^(\*\*Version:\*\*) .*/\1 ${v}/" "$README"
fi
echo " VERSION -> $v"
echo " manifest -> ${ODOO_SERIES}.${v}"
echo " README -> **Version:** ${v}"
}
cmd="${1:-}"; arg="${2:-}"
current="$(read_version)"
case "$cmd" in
major|minor|patch)
IFS='.' read -r MA MI PA <<< "$current"
case "$cmd" in
major) MA=$((MA+1)); MI=0; PA=0 ;;
minor) MI=$((MI+1)); PA=0 ;;
patch) PA=$((PA+1)) ;;
esac
new="${MA}.${MI}.${PA}"
validate "$new"
echo "Bumping ($cmd): $current -> $new"
write_everywhere "$new"
echo
echo "Next: add a '## v${new}' section to CHANGELOG.md, commit, then: scripts/bump-version.sh tag"
;;
set)
[ -n "$arg" ] || die "usage: bump-version.sh set <X.Y.Z>"
validate "$arg"
echo "Setting version: $current -> $arg"
write_everywhere "$arg"
echo
echo "Next: add a '## v${arg}' section to CHANGELOG.md, commit, then: scripts/bump-version.sh tag"
;;
tag)
v="$current"
tagname="v${v}"
git -C "$ROOT" rev-parse "$tagname" >/dev/null 2>&1 && die "tag $tagname already exists"
# Extract this version's section from CHANGELOG (from "## vX.Y.Z" to the next "## v" or "---").
notes="$(awk -v ver="## v${v}" '
$0 ~ "^"ver" " || $0 == ver {flag=1}
flag && /^---[[:space:]]*$/ {exit}
flag && /^## v/ && $0 !~ ver {exit}
flag {print}
' "$CHANGELOG")"
[ -n "$notes" ] || die "no CHANGELOG.md section found for ## v${v}"
echo "Creating annotated tag $tagname with release notes:"
echo "----------------------------------------"
echo "$notes"
echo "----------------------------------------"
git -C "$ROOT" tag -a "$tagname" -m "$notes"
echo "Tagged $tagname. Push with: git push origin $tagname"
;;
""|-h|--help|help)
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
echo "current version: $current"
;;
*)
die "unknown command '$cmd' (try: major | minor | patch | set <v> | tag | help)"
;;
esac