Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ jobs:
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
# For minor/major releases, a new per-minor template dir (e.g. template/v4/v4.5/)
# is created during artifact generation. Stage ./template so it is persisted to the
# release branch; otherwise it is discarded when the runner is torn down.
git add ./build_artifacts ./template
# Only build_artifacts/ is generated. Templates come from main and are never created here.
git add ./build_artifacts
git diff-index --quiet HEAD || git commit -m 'chore: Generate build artifacts for ${{ steps.calc_target.outputs.target_version }} release'
git push --set-upstream origin release-${{ steps.calc_target.outputs.target_version }}
- name: Create or fetch release PR
Expand Down
16 changes: 12 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ template/

### Deciding where to make your change

Where you put a template change determines how far forward it reaches. The key fact: a given minor's template feeds **all future patches of that same minor**, but a *new* minor's template is created by copying the previous minor's template only once (see "How new minor version templates are auto-created at build time" below). So **to reach future minor/major versions, your change must live in the newest minor template** — if the current newest minor is already released, that means creating the next minor's template and putting the change there.
Where you put a template change determines how far forward it reaches. The key fact: a given minor's template feeds **all future patches of that same minor**, but a *new* minor's template is created by copying the previous minor's template only once (see "How new minor version templates are created" below). So **to reach future minor/major versions, your change must live in the newest minor template** — if the current newest minor is already released, that means creating the next minor's template and putting the change there.

First, find the latest minor version that has a template directory (the highest `v4.N`):

Expand Down Expand Up @@ -170,7 +170,7 @@ There are two possible states:

Include every template directory you created or edited in your PR, and open the PR against `main`. Because a new minor's template is created by copying the previous minor's template, landing your change in the newest minor template is what carries it into every minor created afterward.

> **Why this rule exists:** templates are copied forward only **once** — at the moment a new minor version is first created (see "How new minor version templates are auto-created at build time" below). After that, each minor's template is an independent, frozen snapshot. So editing an *already-released* minor's template does not reach any newer minor. A common past mistake was editing v4.5 while v4.6 was already in flight: the change landed only in the v4.5 line and never reached v4.6+. Following the rule above avoids this — you always land the change on the newest minor template, creating it first if the current newest is already released.
> **Why this rule exists:** templates are copied forward only **once** — at the moment a new minor version is first created (see "How new minor version templates are created" below). After that, each minor's template is an independent, frozen snapshot. So editing an *already-released* minor's template does not reach any newer minor. A common past mistake was editing v4.5 while v4.6 was already in flight: the change landed only in the v4.5 line and never reached v4.6+. Following the rule above avoids this — you always land the change on the newest minor template, creating it first if the current newest is already released.

The "Check template propagation" check backs this up on every pull request to `main`: if your PR changes a file in an older minor's template, it fails unless the same file also changes in the newest minor's template. The failure lists each file that is missing the change. For a change that is intentionally scoped to older minor lines only (e.g. a targeted backport), ask a maintainer to add the `template-propagation-scoped` label to your PR; the check reruns and passes. The check only confirms that the newest minor's file changed too, so reviewers still need to confirm it changed the same way.

Expand All @@ -188,9 +188,17 @@ template/v4/v4.7/Dockerfile # including any next-minor template you just creat

Applying the fix to each template explicitly (rather than relying on copy-forward) is intentional — it makes the scope of the fix auditable and prevents accidental feature leakage.

### How new minor version templates are auto-created at build time
### How new minor version templates are created

If no one creates the template directory before `create-minor-version-artifacts` runs, the tooling will automatically copy it from the previous minor's template (e.g., v4.6 → v4.7). This copy happens exactly **once per minor**, when that minor's template is first created, and only ever copies from the immediately-preceding minor. There is no ongoing sync: once a minor's template exists, later edits to an older minor's template are never propagated into it. That is exactly why the rule above has you land your change on the newest minor template.
A new minor's template is created on `main`, by copying the previous minor's template, before that minor's first build. When `X.Y.0` is released, open a pull request to `main` that only does this:

```shell
cp -r template/vX/vX.Y template/vX/vX.(Y+1)
```

Merge it before the first `X.(Y+1).0` build. `create-minor-version-artifacts` does not create a missing template; it fails and prints this command. Before merging, check that the copy is still exact (`diff -r template/vX/vX.Y template/vX/vX.(Y+1)` prints nothing), because a change to `vX.Y` merged while the copy is in review would otherwise be lost. Make changes specific to the new minor in a separate pull request after the copy merges. The first minor of a new major version (`X.0`) is created by hand.

The copy happens exactly **once per minor**. There is no ongoing sync: once a minor's template exists, later edits to an older minor's template are never propagated into it. That is why the rule above has you land your change on the newest minor template. Open template pull requests against `main`, never against a release branch; the build merges `main` into the release branch.


## Finding contributions to work on
Expand Down
32 changes: 13 additions & 19 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,11 @@ def _delete_all_files_except_additional_packages_input_files(base_version_dir, v


def _ensure_minor_template_exists(version: Version):
"""Ensure the per-minor template directory exists for a new minor/major version.
"""Fail unless the per-minor template directory for a new minor/major version exists on main.

If the directory already exists (e.g. a contributor pre-created it with
feature changes), it is left as-is. Otherwise, copies from the previous
minor's template (e.g. template/v4/v4.2/ -> template/v4/v4.3/).

For the first minor of a new major version (minor == 0), the directory must
be created manually since there is no previous minor to copy from.
The build used to copy the previous minor's template here, but that saved the copy only on the
release branch, so template changes merged to main afterwards skipped the new minor (4.6.0 missed
#1343 this way). A new minor's template is now copied on main before its first build.
"""
major = version.major
minor = version.minor
Expand All @@ -93,18 +90,15 @@ def _ensure_minor_template_exists(version: Version):

if minor == 0:
raise Exception(
f"Cannot auto-create template for v{major}.0 — no previous minor exists. "
f"Please create {minor_template_dir}/ manually with a Dockerfile and dirs/."
f"{minor_template_dir}/ does not exist. Create it (a Dockerfile and dirs/) "
f"in a pull request to main before building v{major}.0."
)

prev_minor_template_dir = f"template/v{major}/v{major}.{minor - 1}"
if not os.path.exists(prev_minor_template_dir):
raise Exception(
f"Previous minor template {prev_minor_template_dir}/ does not exist. "
f"Cannot create template for v{major}.{minor}."
)

shutil.copytree(prev_minor_template_dir, minor_template_dir)
raise Exception(
f"{minor_template_dir}/ does not exist. Copy the previous minor's template in a pull request "
f"to main before building v{major}.{minor}.0: "
f"cp -r template/v{major}/v{major}.{minor - 1} {minor_template_dir}"
)


def _create_new_version_artifacts(args):
Expand Down Expand Up @@ -166,8 +160,8 @@ def _copy_static_files(base_version_dir, new_version_dir, new_version_major, new
# - Patch bumps never accidentally gain/lose features from template edits
# meant for newer minors.
# - Security fixes can be applied to specific minor templates explicitly.
# - New minor/major versions get their template created at version-creation time
# (copied from the top-level template/v{major}/ directory).
# - A new minor's template is copied from the previous minor's on main before the
# minor's first build; the build never creates one (see _ensure_minor_template_exists).
base_path = f"template/v{new_version_major}/v{new_version_major}.{new_version_minor}"

for f in glob.glob(os.path.relpath(f"{base_path}/Dockerfile")):
Expand Down
Loading