Skip to content

build(docker): allow third-party addon repos to be pinned by commit sha - #507

Open
reichie020212 wants to merge 2 commits into
19.0from
feat/download-module-commit-sha-pin
Open

build(docker): allow third-party addon repos to be pinned by commit sha#507
reichie020212 wants to merge 2 commits into
19.0from
feat/download-module-commit-sha-pin

Conversation

@reichie020212

Copy link
Copy Markdown
Member

Problem

download_module in docker/Dockerfile can only fetch a branch head. The URL is
hardcoded to the branch namespace:

curl -sSL -o "$tarball" \
    "https://github.com/${repo}/archive/refs/heads/${branch}.tar.gz";

GitHub serves a commit sha from /archive/<sha>.tar.gz and 404s on
/archive/refs/heads/<sha>.tar.gz, so there is no way to pass a sha through this
function:

200  https://github.com/OCA/server-ux/archive/refs/heads/19.0.tar.gz
200  https://github.com/OCA/server-ux/archive/8e5120600987969156c2a59c1ad86bec37318966.tar.gz
404  https://github.com/OCA/server-ux/archive/refs/heads/8e5120600987969156c2a59c1ad86bec37318966.tar.gz

The practical consequence: the six third-party addon repositories are an unpinned
dependency of a pinned image
. Two builds of the same OpenSPP2 commit, a week apart, can
bake in different OCA/server-ux, OCA/server-tools, OCA/rest-framework, … code — and
a deployment that pins OPENSPP2_COMMIT still has no way to say which addon commits it
was tested against. Reproducing a build, or bisecting a regression that came from an OCA
change rather than ours, is not currently possible.

Reported by @reichie020212 while pinning an OpenSPP2 build for the DSWD 4Ps deployment:

I am checking the Dockerfile […] and analyzed the function download_module — based on
the current code, we can only pin OCA resources using a branch unless download_module
is modified to accept commit sha.

Change

  • download_module's third parameter is now a ref: a branch, a tag, or a commit sha
    (short or full). 7-or-more hex digits selects /archive/<ref>.tar.gz; anything else
    keeps today's /archive/refs/heads/<ref>.tar.gz.

  • One ARG <REPO>_REF=19.0 per downloaded repository, so a deployment can pin without
    editing the Dockerfile:

    docker build --build-arg OCA_SERVER_UX_REF=<sha> -f docker/Dockerfile -t openspp .

    A hex-only branch name of 7+ characters (deadbee) would be read as a sha — harmless,
    because GitHub's bare /archive/<rev>.tar.gz resolves branch names as well
    (/archive/19.0.tar.gz → 200); the branch form is kept for non-shas only because it is
    unambiguous between a branch and a like-named tag.

  • docker/README.md documents the arguments under Build.

  • No repository's default ref is changed. Every *_REF defaults to 19.0, which is
    what the call sites resolve today. Which commits to pin is a deployment decision, not
    one this PR makes.

The ARGs sit immediately above the RUN that consumes them, for two reasons: a global
ARG declared before FROM is not in scope inside a stage (under the RUN's set -eu
that is a hard build failure, not an empty string), and declaring them at the top of the
builder stage would invalidate the apt/uv layers whenever a ref changes.

Backwards compatibility

This is the top requirement, so it is asserted rather than argued: the pre-change and
post-change functions were both extracted from the Dockerfile and replayed over all six
production call sites with a stubbed curl recording (tarball, url). The recordings are
byte-identical:

$ diff -u /tmp/rec_old.txt /tmp/rec_new.txt   # no output
/tmp/downloads/x-server-ux-19.0.tar.gz|https://github.com/OCA/server-ux/archive/refs/heads/19.0.tar.gz
/tmp/downloads/x-server-tools-19.0.tar.gz|https://github.com/OCA/server-tools/archive/refs/heads/19.0.tar.gz
/tmp/downloads/x-odoo-job-worker-19.0.tar.gz|https://github.com/OpenSPP/odoo-job-worker/archive/refs/heads/19.0.tar.gz
/tmp/downloads/x-server-backend-19.0.tar.gz|https://github.com/OCA/server-backend/archive/refs/heads/19.0.tar.gz
/tmp/downloads/x-rest-framework-19.0.tar.gz|https://github.com/OCA/rest-framework/archive/refs/heads/19.0.tar.gz
/tmp/downloads/x-muk-it-19.0.tar.gz|https://github.com/muk-it/odoo-modules/archive/refs/heads/19.0.tar.gz

Neither the URLs nor the /tmp/downloads cache keys move, so an existing build cache is
still valid and a build that passes no new argument downloads exactly what it downloads
today.

Verification

The function body was extracted verbatim from the Dockerfile (comment-only lines
stripped the way Docker's parser strips them) and executed under /bin/dash — the real
/bin/sh of the python:3.13-slim-bookworm base — in a throwaway container, against the
live GitHub API:

Check Result
default (no 3rd arg) resolves the pre-change URL …/archive/refs/heads/19.0.tar.gz, 15 addons extracted
explicit branch 19.0 same URL, tree populated
full 40-char sha …/archive/f2d9a5bc….tar.gz, 14 addons extracted
the pin actually pins sha tree differs from branch head in 30 paths
short 7-char sha works; tree identical to the full-sha tree
--strip-components=1 on a sha tarball correct — its root is server-ux-<full sha>/ even for a short sha
cache hit on a repeated ref no second download
cache keys distinct per ref 4 distinct tarballs, all gzip -t clean
ref classification 19.0, 18.0, main, v19.0.1.0.0, abc → branch; deadbee, full sha, upper-case sha → sha
old vs new on all 6 call sites identical (above)
ARG scope + override a stage-scoped ARG reaches the RUN (default 19.0, and an overridden sha); a global ARG referenced in-stage aborts the build under set -eu

Two things I did not verify, stated plainly:

  • No full docker build was run (a shared build host was busy). The function's logic,
    its URL selection, its extraction and the ARG plumbing were each exercised
    individually as above; the end-to-end image build was not.
  • docker buildx build --check --target builder reports no warnings, but a negative
    control showed it does not catch an out-of-scope ARG, so it is evidence that the
    file parses, not that the scoping is right. The scoping was verified with a separate
    minimal build instead.

Known limitation (pre-existing, unchanged)

A ref containing / (e.g. feature/x) breaks the download cache path, because the ref is
interpolated into the tarball filename /tmp/downloads/${dest}-${ref}.tar.gz. This is
true before and after this change — verified against both versions of the function — and
no call site uses such a ref, so it is left alone rather than fixed in passing.

ODOO_SOURCE/ODOO_VERSION (the Odoo/OCB tarball a few lines above) is still
branch-only; extending the same treatment there is deliberately out of scope.

Deploy branch

feat/download-module-commit-sha-pin-on-64e6b31c carries the same commit cherry-picked
onto 64e6b31c (the commit a downstream deployment is currently pinned to), so that
deployment can consume this without taking the 111 commits 19.0 has since gained. This
PR is cut from 19.0 to keep the review clean; docker/Dockerfile and docker/README.md
are identical at both bases, so the cherry-pick was conflict-free.

download_module could only fetch a branch head: it hardcoded the
/archive/refs/heads/<ref>.tar.gz URL, which 404s for a commit sha. The
addon repositories were therefore an unpinned dependency, and two builds
of the same OpenSPP commit could pick up different OCA code.

Give the function a ref that may be a branch, a tag or a commit sha, and
expose one ARG per downloaded repo so a deployment can pin without
editing the Dockerfile:

    docker build --build-arg OCA_SERVER_UX_REF=<sha> ...

A sha carries no ref namespace, so it is served from /archive/<sha>.tar.gz
instead; 7 or more hex digits (short or full sha) selects that form and
anything else keeps today's branch URL. Every default is 19.0, so a build
that passes no new argument resolves exactly the same six URLs and
tarball cache paths as before. No repository's default ref is changed
here — which commits to pin is a deployment decision.

The ARGs sit immediately above the RUN that consumes them so that
changing a ref only invalidates the download layer, and because a global
ARG declared before FROM is not in scope inside the stage.

Signed-off-by: Red <redick@newlogic.com>
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.88%. Comparing base (c4329e2) to head (ed7e236).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #507      +/-   ##
==========================================
- Coverage   76.92%   76.88%   -0.05%     
==========================================
  Files         735      703      -32     
  Lines       47935    45739    -2196     
==========================================
- Hits        36874    35166    -1708     
+ Misses      11061    10573     -488     
Flag Coverage Δ
spp_area ?
spp_area_hdx ?
spp_audit ?
spp_base_common 91.07% <ø> (ø)
spp_programs 67.58% <ø> (ø)
spp_registry 88.94% <ø> (ø)
spp_security 69.56% <ø> (ø)
spp_user_roles ?

Flags with carried forward coverage won't be shown. Click here to find out more.
see 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@reichie020212

Copy link
Copy Markdown
Member Author

About the three red test checks — they are not caused by this change, and they make its case

build and pre-commit pass. test (spp_programs), test (spp_base_common) and
test-summary fail with:

odoo.tools.convert.ParseError: while parsing /mnt/extra-addons/openspp/spp_user_roles/views/user.xml:17
Element '<xpath expr="//field[@name='role_ids']">' cannot be located in parent view

Why it happens. spp_user_roles inherits base_user_role.view_res_users_tree_inherit
and xpaths //field[@name='role_ids']. OCA renamed that field role_ids
user_role_ids in OCA/server-backend on 2026-05-18
(c7e2d1a66fee52c0f942ce9b3aebe51760c59a18, "[19.0][FIX] base_user_role: Cannot see User
Roles"), so the field the xpath looks for has not existed on the 19.0 branch head for
almost four months.

Why 19.0 is nevertheless green. In the last green 19.0 run (df808efa,
run 33846557584) the addon
download layer was reused from the GHA layer cache:

#11 [builder  9/10] RUN --mount=type=cache,target=/tmp/downloads,... download_module() { ...
#11 CACHED

That cached layer holds a pre-rename snapshot of base_user_role. This PR edits that very
RUN instruction, which correctly invalidates the layer, so the build downloads today's
OCA heads for the first time in months — and the pre-existing incompatibility surfaces.
Any PR touching that instruction (or any layer above it) would hit the same wall, and
so would the first cache eviction.

What the same logs prove about backwards compatibility. In this PR's build the layer
ran with no build args and traced:

#18 0.065 + download_module OCA/server-ux server-ux 19.0
#18 0.065 + local url=https://github.com/OCA/server-ux/archive/refs/heads/19.0.tar.gz

— i.e. the pre-change URL, resolved by CI itself, for all six repositories.

Two ways forward, both yours to pick. Fix spp_user_roles for user_role_ids (the
real fix — the tests are telling the truth), or, once this PR is in, pin the addon repo
deliberately, e.g. --build-arg OCA_SERVER_BACKEND_REF=4e3a012a7b6f2516ebde8fdaeada6ae706780c7d (the commit before the
rename) while the module is updated. This PR intentionally changes no default, so it
neither fixes nor worsens the incompatibility — it just stops the layer cache from being
the thing that decides which OCA code you ship.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at ed7e236 (CI green after the branch update picked up the spp_user_roles fix from #508).

Verified

  • The diff is exactly docker/Dockerfile + docker/README.md; the merge commit adds nothing else.
  • Replayed the ref classifier under /bin/dash for 13 refs: 19.0, 18.0, main, abc, 123456refs/heads/…; deadbee, DEADBEE, 1234567 and a full sha → bare /archive/<ref>. Cache keys for the six default call sites are unchanged.
  • Live GitHub: /archive/19.0 and /archive/refs/heads/19.0 → 200; /archive/8e51206 (short sha) → 200 and the tarball root is server-ux-<full sha>/, so --strip-components=1 is right; /archive/deadbee → 404.
  • Nothing else in the repo references download_module or the old URL; ci.yml, ci-full.yml and security.yml build with defaults, so no workflow changes are needed.

Must fix before merge

1. Tags are documented but not routed. The README and the Dockerfile comment say a *_REF accepts "a branch, a tag or a commit sha". A tag is non-hex, so it takes the refs/heads/ route and 404s. Checked live on OpenSPP/odoo-job-worker tag 2026.08:

404  /archive/refs/heads/2026.08.tar.gz
200  /archive/refs/tags/2026.08.tar.gz
200  /archive/2026.08.tar.gz

Either drop "tag" from both places, or send every non-branch ref through the bare /archive/<ref>.tar.gz form, which resolves branches, tags and shas alike. A deployer following the README as written would hit the cryptic tar failure below.

Suggestions (non-blocking)

2. curl -fsSL. Without -f, a 404 stores a 14-byte "Not Found" body and the build dies later in tar with not in gzip format. It is not permanent (the gzip -t check re-downloads next build) but the message hides the cause. Pre-existing, but sha pinning makes a mistyped ref more likely, so worth adding here.

3. README example. Under "pin the addons to exact commits", OCA_SERVER_TOOLS_REF=19.0 is the default branch, not a pin. Either drop that line or use a sha.

Observation, out of scope

With a persistent buildkit cache mount, a branch tarball such as server-ux-19.0.tar.gz is reused as long as it is valid gzip, so a long-lived builder never refreshes branch heads. That is the mirror image of the drift this PR fixes; sha pinning sidesteps it. Worth a separate issue.

Happy to approve as soon as (1) is addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants