Skip to content

chore(manifest): remove obsolete vendor field from template device.to… #87

chore(manifest): remove obsolete vendor field from template device.to…

chore(manifest): remove obsolete vendor field from template device.to… #87

name: ABI Oracle (SemVer Auto-Tagger)
on:
push:
branches:
- main
permissions:
contents: write
concurrency:
group: abi-oracle
cancel-in-progress: false
jobs:
abi-oracle:
name: Calculate ABI and Bump Version
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TARGET_TOKEN }}
- name: Setup Toob Ecosystem
uses: ./.github/actions/setup-cli
with:
install-native-deps: "true"
- name: Configure Git Identity
run: |
git config user.name "Toob ABI Oracle"
git config user.email "oracle@toob-boot.io"
- name: Detect Subsystem Changes
id: changes
run: |
echo ">>> Detecting changes in recent commit..."
git fetch --prune --unshallow 2>/dev/null || git fetch --all 2>/dev/null || true
# Ensure the registry submodule is initialized
if [ ! -d "toob-registry/.git" ] && [ ! -f "toob-registry/.git" ]; then
git submodule update --init toob-registry
fi
# Check if a monorepo subsystem changed (direct files, not submodules).
# Diffs from the last matching tag to capture accumulated changes.
check_subsystem() {
local tag_prefix=$1
shift
local paths=("$@")
local last_tag=$(git describe --tags --match "${tag_prefix}v*" --abbrev=0 2>/dev/null || echo "")
local diff_files=""
if [ -n "$last_tag" ]; then
diff_files=$(git diff --name-only "$last_tag" HEAD 2>/dev/null || echo "")
else
diff_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
if [ -z "$diff_files" ]; then
diff_files=$(git ls-files)
fi
fi
for file in $diff_files; do
for p in "${paths[@]}"; do
if [[ "$file" == $p* ]]; then
echo "true"
return
fi
done
done
echo "false"
}
# Registry subsystem versioning (chips, arch, drivers, toolchains)
# is handled by the Toob-Registry's own CI pipeline:
# main-release.yml → semver_calc.go → build_registry.go
# The enforcer only manages Core SDK, CLI, and Compiler tags.
CHECK_CORE=$(check_subsystem "core/" "toobloader/core/" "sdk/" "common/")
CHECK_CLI=$(check_subsystem "cli/" "cli/toob-cli/")
CHECK_COMPILER=$(check_subsystem "compiler/" "cli/.pipeline-repo/Dockerfile.compiler" "cli/.pipeline-repo/toob-ci-build.sh" "compiler/")
echo "CHECK_CORE=$CHECK_CORE" >> $GITHUB_ENV
echo "CHECK_CLI=$CHECK_CLI" >> $GITHUB_ENV
echo "CHECK_COMPILER=$CHECK_COMPILER" >> $GITHUB_ENV
echo "Core changed: $CHECK_CORE"
echo "CLI changed: $CHECK_CLI"
echo "Compiler changed: $CHECK_COMPILER"
- name: ABI Orakel (Core SDK)
if: env.CHECK_CORE == 'true'
run: |
echo ">>> [Core Oracle] Starting symbol analysis for Core, SDK, and Common..."
LAST_TAG=$(git describe --tags --match "core/v*" --abbrev=0 2>/dev/null || echo "core/v0.0.0")
RAW_VERSION=${LAST_TAG#core/v}
echo ">>> [Core Oracle] Baseline Tag: $LAST_TAG"
MAJOR=$(echo $RAW_VERSION | cut -d. -f1)
MINOR=$(echo $RAW_VERSION | cut -d. -f2)
PATCH=$(echo $RAW_VERSION | cut -d. -f3)
# If no real baseline exists, create the initial tag and skip diff
if [ "$LAST_TAG" = "core/v0.0.0" ]; then
echo ">>> [Core Oracle] No baseline tag. Creating initial patch release."
PATCH=$((PATCH + 1))
NEW_TAG="core/v${MAJOR}.${MINOR}.${PATCH}"
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
echo ">>> [Core Oracle] Tag $NEW_TAG already exists. Skipping."
else
git tag "$NEW_TAG"
git push origin "$NEW_TAG" || echo ">>> Tag push failed (concurrent run?)"
echo ">>> [Core Oracle] Initial tag $NEW_TAG pushed!"
fi
exit 0
fi
mkdir -p abi_workspace/current abi_workspace/old
# Build CURRENT code.
# The esp32c6 template is used as a reference target. Core libraries
# are chip-independent — the ABI output is identical for any chip.
echo ">>> Building CURRENT Code..."
toob build --native --manifest toob-registry/chips/esp32c6/template_device.toml
for lib in libtoob_core.a libtoob_libtoob.a libtoob_zcbor.a libtoob_heatshrink.a; do
find . -path '*/builds/*' -name "$lib" -exec cp {} abi_workspace/current/ \;
done
find . -path '*/builds/build_*' -type d -exec rm -rf {} + 2>/dev/null || true
# Build OLD code using git worktree (faster than clone, shares object store)
echo ">>> Building OLD Code from $LAST_TAG..."
git worktree add abi_workspace/old_repo "$LAST_TAG" 2>/dev/null || {
echo ">>> [FATAL] Cannot create worktree for $LAST_TAG"
exit 1
}
cd abi_workspace/old_repo
git submodule update --init toob-registry
toob build --native --manifest toob-registry/chips/esp32c6/template_device.toml || {
echo ">>> [FATAL] Old build failed. Cannot produce ABI baseline."
cd ../../
git worktree remove abi_workspace/old_repo --force 2>/dev/null || true
exit 1
}
for lib in libtoob_core.a libtoob_libtoob.a libtoob_zcbor.a libtoob_heatshrink.a; do
find . -path '*/builds/*' -name "$lib" -exec cp {} ../old/ \;
done
cd ../../
git worktree remove abi_workspace/old_repo --force 2>/dev/null || true
# Symbol-based ABI diff using the cross-compiler's nm.
# This avoids abidiff's crash on cross-compiled RISC-V ELF files.
# Extract public symbols (T=text, D=data, B=bss) and compare sets.
echo ">>> Running symbol-based ABI diff..."
# Locate the cross-compiler nm (installed during build)
NM_BIN=$(find ~/.toob/toolchains -name "riscv32-esp-elf-nm" 2>/dev/null | head -1)
if [ -z "$NM_BIN" ]; then
NM_BIN="nm" # fallback to host nm
fi
echo ">>> Using nm: $NM_BIN"
BUMP="PATCH"
for lib in libtoob_core.a libtoob_libtoob.a libtoob_zcbor.a libtoob_heatshrink.a; do
OLD_LIB="abi_workspace/old/$lib"
NEW_LIB="abi_workspace/current/$lib"
if [ ! -f "$OLD_LIB" ] || [ ! -f "$NEW_LIB" ]; then
continue
fi
# Extract globally-visible defined symbols (type + name)
$NM_BIN --defined-only -g "$OLD_LIB" 2>/dev/null | awk '{print $2, $3}' | sort -u > /tmp/sym_old.txt
$NM_BIN --defined-only -g "$NEW_LIB" 2>/dev/null | awk '{print $2, $3}' | sort -u > /tmp/sym_new.txt
REMOVED=$(comm -23 /tmp/sym_old.txt /tmp/sym_new.txt | wc -l)
ADDED=$(comm -13 /tmp/sym_old.txt /tmp/sym_new.txt | wc -l)
if [ "$REMOVED" -gt 0 ]; then
echo ">>> [RESULT] $lib: $REMOVED symbols REMOVED (MAJOR)"
comm -23 /tmp/sym_old.txt /tmp/sym_new.txt | head -10
BUMP="MAJOR"
elif [ "$ADDED" -gt 0 ] && [ "$BUMP" != "MAJOR" ]; then
echo ">>> [RESULT] $lib: $ADDED symbols ADDED (MINOR)"
comm -13 /tmp/sym_old.txt /tmp/sym_new.txt | head -10
[ "$BUMP" = "PATCH" ] && BUMP="MINOR"
else
echo ">>> [RESULT] $lib: No symbol changes"
fi
done
# Layer 2: Public header diff to catch struct/typedef/enum changes
# that are invisible to nm (same symbol name, different layout).
echo ">>> Running public header analysis..."
HEADER_DIRS="toobloader/core/include sdk/libtoob/include sdk/os_client/include common/include"
for hdir in $HEADER_DIRS; do
[ ! -d "$hdir" ] && continue
DELETED_H=$(git diff --diff-filter=D --name-only "$LAST_TAG" HEAD -- "$hdir" 2>/dev/null | wc -l)
ADDED_H=$(git diff --diff-filter=A --name-only "$LAST_TAG" HEAD -- "$hdir" 2>/dev/null | wc -l)
MODIFIED_H=$(git diff --diff-filter=M --name-only "$LAST_TAG" HEAD -- "$hdir" 2>/dev/null || echo "")
# Deleted public headers = breaking
if [ "$DELETED_H" -gt 0 ]; then
echo ">>> [HEADER] $hdir: $DELETED_H headers DELETED (MAJOR)"
BUMP="MAJOR"
fi
# New public headers = new capability
if [ "$ADDED_H" -gt 0 ] && [ "$BUMP" != "MAJOR" ]; then
echo ">>> [HEADER] $hdir: $ADDED_H new headers (MINOR)"
[ "$BUMP" = "PATCH" ] && BUMP="MINOR"
fi
# Modified headers: check if struct/typedef/enum definitions changed
if [ -n "$MODIFIED_H" ] && [ "$BUMP" != "MAJOR" ]; then
for hfile in $MODIFIED_H; do
# Extract struct, typedef, and enum lines from old and new
OLD_DEFS=$(git show "${LAST_TAG}:${hfile}" 2>/dev/null | grep -E '^\s*(typedef\s|struct\s|enum\s|}\s*\w+)' | sort)
NEW_DEFS=$(cat "$hfile" 2>/dev/null | grep -E '^\s*(typedef\s|struct\s|enum\s|}\s*\w+)' | sort)
if [ "$OLD_DEFS" != "$NEW_DEFS" ]; then
echo ">>> [HEADER] $hfile: type definitions changed (MINOR)"
[ "$BUMP" = "PATCH" ] && BUMP="MINOR"
fi
done
fi
done
echo ">>> [Core Oracle] Final bump type: $BUMP"
case "$BUMP" in
MAJOR) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
MINOR) MINOR=$((MINOR + 1)); PATCH=0 ;;
PATCH) PATCH=$((PATCH + 1)) ;;
esac
NEW_TAG="core/v${MAJOR}.${MINOR}.${PATCH}"
echo ">>> [Core Oracle] Calculated new version: $NEW_TAG"
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
echo ">>> [Core Oracle] Tag $NEW_TAG already exists. Skipping."
else
git tag "$NEW_TAG"
git push origin "$NEW_TAG" || echo ">>> Tag push failed (concurrent run?)"
echo ">>> [Core Oracle] Tag $NEW_TAG pushed!"
fi
- name: ABI Orakel (CLI)
if: env.CHECK_CLI == 'true'
run: |
echo ">>> [CLI Oracle] Starting Contract analysis for CLI..."
LAST_TAG=$(git describe --tags --match "cli/v*" --abbrev=0 2>/dev/null || echo "cli/v0.0.0")
RAW_VERSION=${LAST_TAG#cli/v}
echo ">>> [CLI Oracle] Baseline Tag: $LAST_TAG"
MAJOR=$(echo $RAW_VERSION | cut -d. -f1)
MINOR=$(echo $RAW_VERSION | cut -d. -f2)
PATCH=$(echo $RAW_VERSION | cut -d. -f3)
mkdir -p cli_abi_workspace
cp cli/toob-cli/internal/ports/ports.go cli_abi_workspace/ports_current.go
if [ "$LAST_TAG" != "cli/v0.0.0" ]; then
git show $LAST_TAG:cli/toob-cli/internal/ports/ports.go > cli_abi_workspace/ports_old.go 2>/dev/null || touch cli_abi_workspace/ports_old.go
else
touch cli_abi_workspace/ports_old.go
fi
echo ">>> Running AST-based Contract Differ..."
cd cli/toob-cli
go build -o /tmp/semver-tool ./internal/ports/cmd/semver
BUMP_TYPE=$(/tmp/semver-tool ../../cli_abi_workspace/ports_old.go ../../cli_abi_workspace/ports_current.go | tail -n 1)
cd ../../
echo ">>> [RESULT] Contract Differ determined bump type: $BUMP_TYPE"
if [ "$BUMP_TYPE" = "MAJOR" ]; then
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0
elif [ "$BUMP_TYPE" = "MINOR" ]; then
MINOR=$((MINOR + 1)); PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_TAG="cli/v${MAJOR}.${MINOR}.${PATCH}"
echo ">>> [CLI Oracle] Calculated new version: $NEW_TAG"
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
echo ">>> [CLI Oracle] Tag $NEW_TAG already exists. Skipping."
else
git tag "$NEW_TAG"
git push origin "$NEW_TAG" || echo ">>> Tag push failed (concurrent run?)"
echo ">>> [CLI Oracle] Tag $NEW_TAG pushed!"
fi
- name: ABI Orakel (Compiler Container)
if: env.CHECK_COMPILER == 'true'
run: |
echo ">>> [Compiler Oracle] Starting manifest-based version analysis..."
LAST_TAG=$(git describe --tags --match "compiler/v*" --abbrev=0 2>/dev/null || echo "compiler/v0.0.0")
echo ">>> [Compiler Oracle] Baseline Tag: $LAST_TAG"
CURRENT_VERSION=$(jq -r '.compiler_version' compiler/compiler_manifest.json)
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
BUMP="PATCH"
if [ "$LAST_TAG" != "compiler/v0.0.0" ]; then
OLD_MANIFEST=$(git show $LAST_TAG:compiler/compiler_manifest.json 2>/dev/null || echo '{}')
OLD_PROTOCOL=$(echo "$OLD_MANIFEST" | jq -r '.protocol_version // 0')
NEW_PROTOCOL=$(jq -r '.protocol_version' compiler/compiler_manifest.json)
if [ "$OLD_PROTOCOL" != "$NEW_PROTOCOL" ]; then
BUMP="MAJOR"
fi
if [ "$BUMP" = "PATCH" ]; then
OLD_BASE=$(echo "$OLD_MANIFEST" | jq -r '.base_image.image // ""')
NEW_BASE=$(jq -r '.base_image.image' compiler/compiler_manifest.json)
OLD_SYSPKG=$(echo "$OLD_MANIFEST" | jq -cS '.system_packages // []')
NEW_SYSPKG=$(jq -cS '.system_packages' compiler/compiler_manifest.json)
if [ "$OLD_BASE" != "$NEW_BASE" ] || [ "$OLD_SYSPKG" != "$NEW_SYSPKG" ]; then
BUMP="MINOR"
fi
fi
fi
echo ">>> [Compiler Oracle] Bump type: $BUMP"
if [ "$BUMP" = "MAJOR" ]; then
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0
elif [ "$BUMP" = "MINOR" ]; then
MINOR=$((MINOR + 1)); PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
NEW_TAG="compiler/v${NEW_VERSION}"
echo ">>> [Compiler Oracle] New version: $NEW_TAG"
# Check tag existence BEFORE writing to manifest to prevent double-bumps
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
echo ">>> [Compiler Oracle] Tag $NEW_TAG already exists. Skipping."
exit 0
fi
# Update version in manifest and commit
jq --arg v "$NEW_VERSION" '.compiler_version = $v' \
compiler/compiler_manifest.json > tmp_manifest.json \
&& mv tmp_manifest.json compiler/compiler_manifest.json
git add compiler/compiler_manifest.json
git commit -m "chore(compiler): bump to v${NEW_VERSION} [skip ci]" || true
# Atomic push: commit + tag together to prevent race conditions
git tag "$NEW_TAG"
git push origin HEAD:main "$NEW_TAG" || echo ">>> Push failed (concurrent run?)"
echo ">>> [Compiler Oracle] Tag $NEW_TAG pushed!"
# Create draft release as manual approval gate
CLI_VER=$(jq -r '.cli.version' compiler/compiler_manifest.json)
PROTO_VER=$(jq -r '.protocol_version' compiler/compiler_manifest.json)
BODY="## Compiler v${NEW_VERSION}\n\n"
BODY+="| Property | Value |\n|---|---|\n"
BODY+="| CLI | v${CLI_VER} |\n"
BODY+="| Protocol | v${PROTO_VER} |\n\n"
BODY+="**Publish this release to trigger the Docker image build on the CI server.**"
curl -sf -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.RELEASE_TARGET_TOKEN }}" \
https://api.github.com/repos/Toob-Boot/Toob-Loader/releases \
-d "{
\"tag_name\": \"${NEW_TAG}\",
\"name\": \"Compiler v${NEW_VERSION}\",
\"body\": \"$(echo -e "$BODY")\",
\"draft\": true,
\"prerelease\": false
}" && echo ">>> [Compiler Oracle] Draft release created for ${NEW_TAG}" \
|| echo ">>> [Compiler Oracle] Draft creation failed (non-critical)"