215 lines
9.7 KiB
YAML
215 lines
9.7 KiB
YAML
# Pillar 2 / Phase 4 — Cold-start compat-matrix seed.
|
|
#
|
|
# This file is the SOURCE-OF-TRUTH copy. The deployed copy lives in
|
|
# the Gitea repo at:
|
|
#
|
|
# odoo-tower/odoo-addons (branch `compat-bootstrap`)
|
|
# └── .gitea/workflows/seed-compat.yml
|
|
#
|
|
# Why a dedicated branch instead of `main`/`18.0`/`19.0`: the addon
|
|
# code lives on per-Odoo-major branches. The cold-start snapshot is
|
|
# orthogonal data — keeping it on its own branch lets Tower fetch
|
|
# from one stable place and lets the cron commit without touching
|
|
# addon source.
|
|
#
|
|
# Schedule: daily 03:00 UTC. The emitter computes a content-hash
|
|
# stampId, so identical results across nights produce no commit
|
|
# (git diff is empty → push is skipped). When the catalog drifts
|
|
# (a new addon, a fix, a regression), one commit lands and Tower's
|
|
# next 24h tick replays it.
|
|
#
|
|
# Deployment (one-shot, manual until we mass-publish workflows):
|
|
#
|
|
# git -C /path/to/odoo-addons checkout -b compat-bootstrap origin/18.0
|
|
# mkdir -p .gitea/workflows compat-bootstrap
|
|
# cp infrastructure/gitea-actions/workflows/seed-compat.yml \
|
|
# /path/to/odoo-addons/.gitea/workflows/
|
|
# cp scripts/qualify-addon.py scripts/emit-compat-bootstrap.py \
|
|
# /path/to/odoo-addons/scripts/
|
|
# git -C /path/to/odoo-addons add .gitea scripts compat-bootstrap
|
|
# git -C /path/to/odoo-addons commit -m "feat(compat): seed bootstrap workflow"
|
|
# git -C /path/to/odoo-addons push -u origin compat-bootstrap
|
|
|
|
name: Cold-start compat seed
|
|
on:
|
|
schedule:
|
|
- cron: '0 3 * * *' # nightly 03:00 UTC
|
|
workflow_dispatch: {} # manual trigger from Gitea UI
|
|
|
|
jobs:
|
|
seed:
|
|
runs-on: ubuntu-latest
|
|
# LOW — bound the worst-case runtime so a hung git fetch can't
|
|
# block the next nightly tick from starting.
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout compat-bootstrap branch
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: compat-bootstrap
|
|
fetch-depth: 1
|
|
|
|
- name: Materialise per-major addon trees
|
|
run: |
|
|
set -euo pipefail
|
|
# Detached worktrees keep each version branch's tree independent
|
|
# so the qualifier sees a clean addon root with no cross-branch
|
|
# contamination. Cleaned up in the final step.
|
|
for major in 18.0 19.0; do
|
|
git fetch --depth=1 origin "$major"
|
|
git worktree add --detach "addons-$major" "origin/$major"
|
|
ls -1 "addons-$major/addons" 2>/dev/null | head -5 || true
|
|
done
|
|
|
|
- name: Run qualifier + emit bootstrap snapshot per major
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p compat-bootstrap/per-major
|
|
for major in 18.0 19.0; do
|
|
# qualify-addon.py is vendored into each version branch under
|
|
# .gitea/qualify-addon.py by Pillar 1; reuse that copy so static-
|
|
# lint logic stays in lockstep with what addon-qualify.yml runs
|
|
# on push.
|
|
python3 scripts/emit-compat-bootstrap.py \
|
|
--addons-root "addons-$major/addons" \
|
|
--qualifier "addons-$major/.gitea/qualify-addon.py" \
|
|
--output "compat-bootstrap/per-major/$major.json" \
|
|
--pg-by-major '{"18.0":"16","19.0":"17"}' \
|
|
--source "qualify-addon-v1" \
|
|
--min-rows 5
|
|
done
|
|
|
|
- name: Merge per-major snapshots into seeded-ci.json
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'PY'
|
|
import hashlib, json, pathlib
|
|
rows = []
|
|
for p in sorted(pathlib.Path('compat-bootstrap/per-major').glob('*.json')):
|
|
rows.extend(json.loads(p.read_text())['rows'])
|
|
rows.sort(key=lambda r: (r['addonCode'], r['addonVersion'],
|
|
r['odooMajor'], r['postgresMajor']))
|
|
canonical = json.dumps(rows, sort_keys=True, separators=(',', ':'))
|
|
stamp = 'sha256:' + hashlib.sha256(canonical.encode('utf-8')).hexdigest()
|
|
out = {
|
|
'stampId': stamp,
|
|
'schemaVersion': 1,
|
|
'source': 'qualify-addon-v1',
|
|
'rows': rows,
|
|
}
|
|
pathlib.Path('compat-bootstrap/seeded-ci.json').write_text(
|
|
json.dumps(out, indent=2) + '\n')
|
|
print(f'merged {len(rows)} rows; stamp={stamp}')
|
|
PY
|
|
|
|
- name: Install cosign
|
|
run: |
|
|
set -euo pipefail
|
|
# H3 — pin the v2.4.1 cosign-linux-amd64 SHA256 and verify
|
|
# before chmod+exec. Without this, a MITM (or a compromised
|
|
# GitHub edge cache) can substitute a binary that exfiltrates
|
|
# COSIGN_PRIVATE_KEY on the next sign-blob call.
|
|
# Cross-checked against
|
|
# https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign_checksums.txt
|
|
# on 2026-05-10.
|
|
COSIGN_SHA256=8b24b946dd5809c6bd93de08033bcf6bc0ed7d336b7785787c080f574b89249b
|
|
if ! command -v cosign >/dev/null 2>&1; then
|
|
curl -sSL -o /tmp/cosign \
|
|
https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64
|
|
echo "$COSIGN_SHA256 /tmp/cosign" | sha256sum -c -
|
|
mv /tmp/cosign /usr/local/bin/cosign
|
|
chmod +x /usr/local/bin/cosign
|
|
fi
|
|
cosign version | head -3
|
|
|
|
- name: Sign seeded-ci.json (Phase 4.1)
|
|
env:
|
|
COSIGN_PRIVATE_KEY: ${{ secrets.COMPAT_SIGNING_KEY }}
|
|
COSIGN_PASSWORD: ${{ secrets.COMPAT_SIGNING_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ ! -f compat-bootstrap/seeded-ci.json ]; then
|
|
echo "ERROR: seeded-ci.json was not produced; nothing to sign"
|
|
exit 1
|
|
fi
|
|
# cosign sign-blob with --key env://VAR reads the private key
|
|
# PEM from the named env var; the file/stdout split keeps the
|
|
# raw key out of process args (where it'd show up in `ps`).
|
|
cosign sign-blob --yes \
|
|
--key env://COSIGN_PRIVATE_KEY \
|
|
--output-signature compat-bootstrap/seeded-ci.json.sig \
|
|
compat-bootstrap/seeded-ci.json
|
|
# H4 — Self-verify is a key-rotation TRIPWIRE, NOT a trust
|
|
# anchor. The on-disk cosign.pub lives in the same branch we
|
|
# push to; anyone holding COMPAT_PUSH_TOKEN can replace it.
|
|
# The real verification is Tower-side, against the pubkey
|
|
# baked into the binary at backend/cmd/api/compat_bootstrap_pubkey.pem
|
|
# (compat_seed_loader.go:38, go:embed). This step only catches
|
|
# "the operator rotated the keypair on lab1 but forgot to
|
|
# update either the Gitea secrets or the on-disk pubkey" —
|
|
# cheap CI-time signal so the next Tower deploy doesn't
|
|
# surprise-fail the verifier.
|
|
if [ -f compat-bootstrap/cosign.pub ]; then
|
|
cosign verify-blob --insecure-ignore-tlog \
|
|
--key compat-bootstrap/cosign.pub \
|
|
--signature compat-bootstrap/seeded-ci.json.sig \
|
|
compat-bootstrap/seeded-ci.json
|
|
fi
|
|
echo "signed: $(wc -c < compat-bootstrap/seeded-ci.json.sig) bytes"
|
|
|
|
- name: Commit + push if content changed
|
|
env:
|
|
GIT_USER_TOKEN: ${{ secrets.COMPAT_PUSH_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# M3 — worktree cleanup runs even if a prior step crashed
|
|
# (a `trap` would be cleaner, but we're already in a fresh
|
|
# step with `set -e`; the loop just no-ops on absent paths).
|
|
for major in 18.0 19.0; do git worktree remove --force "addons-$major" || true; done
|
|
rm -rf compat-bootstrap/per-major
|
|
|
|
# Author identity must match a real Gitea user — the
|
|
# branch-protection pre-receive hook calls GetUserByEmail
|
|
# against the committer and rejects with "Internal Server
|
|
# Error" (uid=-2) when no match exists. The COMPAT_PUSH_TOKEN
|
|
# belongs to git_admin, whose canonical Gitea email is
|
|
# `gitea@local.domain` (the install default — NOT the address
|
|
# in .credentials.md, which never made it onto the user
|
|
# record). Commit + push under that identity for a clean
|
|
# audit trail.
|
|
git config user.email "gitea@local.domain"
|
|
git config user.name "git_admin"
|
|
|
|
if [ ! -f compat-bootstrap/seeded-ci.json ]; then
|
|
echo "ERROR: seeded-ci.json was not produced; aborting"
|
|
exit 1
|
|
fi
|
|
|
|
# M1 — diff check against the JSON ONLY. Cosign's ECDSA
|
|
# sign-blob uses random k by default, so the .sig is fresh
|
|
# bytes every run even when the JSON is byte-identical.
|
|
# Including the .sig in the diff would push a noise commit
|
|
# nightly. Stage the .sig first so it's part of the commit
|
|
# IF we end up making one, but only the .json drives the
|
|
# decision.
|
|
git add compat-bootstrap/seeded-ci.json
|
|
if git diff --cached --quiet -- compat-bootstrap/seeded-ci.json; then
|
|
echo "no content change in seeded-ci.json; nothing to commit"
|
|
exit 0
|
|
fi
|
|
git add compat-bootstrap/seeded-ci.json.sig
|
|
|
|
stamp=$(python3 -c "import json,sys; print(json.load(open('compat-bootstrap/seeded-ci.json'))['stampId'])")
|
|
git commit -m "chore(compat): refresh cold-start seed (${stamp})"
|
|
|
|
# H5 — push via http.extraHeader, not credential-in-URL.
|
|
# If git push fails (network timeout, ref reject, redirect
|
|
# following), the URL is echoed in error context AND captured
|
|
# by the runner's stderr log; embedding the token in the URL
|
|
# leaks it in any of those paths. extraHeader is git-internal,
|
|
# never echoed.
|
|
git push \
|
|
-c "http.extraHeader=Authorization: token ${GIT_USER_TOKEN}" \
|
|
https://git.odoosky.org/odoo-tower/odoo-addons.git \
|
|
HEAD:compat-bootstrap
|