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 | | Model | Field | Odoo 16 | Odoo 17 | Note |
|-------|-------|---------|---------|------| |-------|-------|---------|---------|------|
| `blog.post` | `body_arch` | Yes | **No** | XML layout field; removed in Odoo 17 | | `blog.post` | `body_arch` | Yes | **No** | Removed in Odoo 17 |
| `blog.post` | `body` | Yes | **No** | HTML body field; use editor interface instead | | `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 | | `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:** **Pattern for extending blog.post safely:**
```python ```python
@ -817,15 +820,24 @@ class BlogPost(models.Model):
**Pattern for creating blog.post test records:** **Pattern for creating blog.post test records:**
```python ```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({ post = self.env['blog.post'].sudo().create({
'name': 'Test Post', 'name': 'Test Post',
'blog_id': blog.id, 'blog_id': blog.id,
'is_published': True, '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 ## 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:** **Add Phase 2 (BDD, factories, coverage gates) when ≥ 2:**
- Stakeholders ask "how do we know X works?" - Stakeholders ask "how do we know X works?"

View file

@ -1,7 +1,11 @@
# ITSulu Blog Publisher — Odoo 17 Addon # 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). 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 ## Features
### ✨ Core Functionality ### ✨ Core Functionality
@ -28,13 +32,14 @@ Automated blog post generation and publishing for Odoo 17 Community Edition, pow
### 📊 Test Coverage ### 📊 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 | | Test Suite | Count | Status |
|---|---|---| |---|---|---|
| Unit Tests (TDD) | 48 | ✅ All Passing | | Unit Tests (TDD) | 48 | ✅ All Passing |
| Behavior Tests (BDD) | 15 | ✅ All Passing | | Behaviour Tests (BDD) | 15 | ✅ All Passing |
| **Total** | **63** | **100% ✅** | | Performance Benchmarks | 6 | ✅ All Passing |
| **Total** | **69** | **100% ✅** |
**Coverage Areas**: **Coverage Areas**:
- Topic queue management (7 tests) - 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) - Social media copy (16 tests)
- Schedule slots (10 tests) - Schedule slots (10 tests)
- LLM router dispatch (7 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 ## Installation
@ -232,7 +241,8 @@ pre-commit run --all-files
## Documentation ## 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 - [ARCHITECTURE.md](ARCHITECTURE.md) — System design, data flow, API contracts
- [PHASE2_ROADMAP.md](PHASE2_ROADMAP.md) — Phase 2 implementation status - [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 **Last Updated**: 2026-05-30
**Maintainer**: Nicholas Riegel (nicholasr@itsulu.com) **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 -*- # -*- coding: utf-8 -*-
{ {
'name': 'ITSulu Blog Publisher', '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', 'summary': 'AI-powered blog post generation with multi-LLM support, scheduling, and social media copy',
'description': """ 'description': """
ITSulu Blog Publisher 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