← Files Dataiku HeadlessARCHIVED FILE

tests/test_release.py

11.1 KB · Nov 11, 2026 · 12:58 UTC

↓ Download file

See the change to this file →

# Copyright 2026 Dataiku SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Release validation and publishing against disposable local git repositories."""

import json
import os
from pathlib import Path
import runpy
import shutil
import subprocess

import pytest
import yaml

ROOT = Path(__file__).resolve().parents[1]
HELPERS = runpy.run_path(str(ROOT / "scripts/check_release.py"))


@pytest.fixture
def release_tree(tmp_path):
    for name in ("pyproject.toml", "CHANGELOG.md"):
        shutil.copy(ROOT / name, tmp_path / name)
    for path in [*ROOT.glob(".*-plugin/*.json"), *ROOT.glob(".github/plugin/*.json")]:
        target = tmp_path / path.relative_to(ROOT)
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(path, target)
    return tmp_path


def test_current_release_and_notes(release_tree):
    version = HELPERS["validate"](release_tree)
    notes = HELPERS["release_notes"](release_tree, version)
    assert notes.startswith(f"## v{version} ")
    assert notes.count("\n## ") == 0


@pytest.mark.parametrize(
    "directory",
    [
        ".claude-plugin",
        ".codex-plugin",
        ".cortex-plugin",
        ".cursor-plugin",
        ".github/plugin",
    ],
)
def test_reject_manifest_version_drift(release_tree, directory):
    path = release_tree / directory / "plugin.json"
    manifest = json.loads(path.read_text())
    manifest["version"] = "0.0.1"
    path.write_text(json.dumps(manifest))
    with pytest.raises(ValueError, match="must carry version"):
        HELPERS["validate"](release_tree)


@pytest.mark.parametrize("directory", [".cursor-plugin", ".github/plugin"])
def test_reject_missing_bump_configuration(release_tree, directory):
    path = release_tree / "pyproject.toml"
    path.write_text(path.read_text().replace(f'    "{directory}/plugin.json",\n', ""))
    with pytest.raises(ValueError, match="version_files"):
        HELPERS["validate"](release_tree)


@pytest.mark.parametrize("directory", [".claude-plugin", ".github/plugin"])
def test_reject_marketplace_pin(release_tree, directory):
    path = release_tree / directory / "marketplace.json"
    data = json.loads(path.read_text())
    data["plugins"][0]["version"] = "0.0.1"
    path.write_text(json.dumps(data))
    with pytest.raises(ValueError, match="Marketplace"):
        HELPERS["validate"](release_tree)


def test_reject_missing_github_manifest(release_tree):
    (release_tree / ".github/plugin/plugin.json").unlink()
    with pytest.raises(ValueError, match="Missing release manifest"):
        HELPERS["validate"](release_tree)


def test_reject_missing_changelog(release_tree):
    with pytest.raises(ValueError, match="No changelog section"):
        HELPERS["release_notes"](release_tree, "999.0.0")


def git(root, *args):
    return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()


def init_repo(root):
    git(root, "init", "-b", "main")
    git(root, "config", "user.name", "Release Test")
    git(root, "config", "user.email", "release@example.invalid")
    git(root, "config", "commit.gpgsign", "false")
    git(root, "config", "tag.gpgsign", "false")
    git(root, "config", "core.hooksPath", "/dev/null")
    git(root, "add", ".")
    git(root, "commit", "-m", "chore: baseline")


def test_commitizen_updates_all_manifests_without_publishing(release_tree):
    init_repo(release_tree)
    current = HELPERS["validate"](release_tree)
    git(release_tree, "tag", f"v{current}")
    git(release_tree, "commit", "--allow-empty", "-m", "feat: next release")
    major, minor, _ = map(int, current.split("."))
    target = f"{major}.{minor + 1}.0"
    branch = f"release/{target}"
    git(release_tree, "switch", "-c", branch)
    assert HELPERS["validate_target_branch"](branch, current) == target
    result = subprocess.run(
        [shutil.which("cz"), "bump", target, "--version-files-only", "--changelog"],
        cwd=release_tree,
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    version = HELPERS["validate"](release_tree)
    assert version == target
    assert HELPERS["release_notes"](release_tree, version)
    assert git(release_tree, "tag", "--list") == f"v{current}"


@pytest.mark.parametrize("collision", [False, True])
def test_publish_retry_and_tag_collision(tmp_path, collision):
    repo = tmp_path / "repo"
    repo.mkdir()
    (repo / "file").write_text("release")
    init_repo(repo)
    sha = git(repo, "rev-parse", "HEAD")
    remote = tmp_path / "remote.git"
    subprocess.run(
        ["git", "init", "--bare", str(remote)], check=True, capture_output=True
    )
    git(repo, "remote", "add", "origin", str(remote))
    if collision:
        git(repo, "tag", "dataiku-headless--v1.2.3")
        git(repo, "commit", "--allow-empty", "-m", "fix: another commit")
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    gh = bin_dir / "gh"
    gh.write_text(
        '#!/bin/sh\nif [ "$2" = list ]; then echo "[]"; else echo "release operation"; fi\n'
    )
    gh.chmod(0o755)
    workflow = yaml.safe_load((ROOT / ".github/workflows/bump.yml").read_text())
    script = workflow["jobs"]["publish"]["steps"][-1]["run"]
    env = dict(
        os.environ,
        VERSION="1.2.3",
        RUNNER_TEMP=str(tmp_path),
        GITHUB_STEP_SUMMARY=str(tmp_path / "summary"),
        PATH=f"{bin_dir}:{os.environ['PATH']}",
    )
    for _ in range(2):
        result = subprocess.run(
            ["bash", "-c", script], cwd=repo, env=env, capture_output=True, text=True
        )
        if collision:
            assert result.returncode != 0
            assert "points to another commit" in result.stdout
            assert git(remote, "tag", "--list") == ""
            assert git(repo, "tag", "--list", "v1.2.3") == ""
        else:
            assert result.returncode == 0, result.stderr
            for tag in ("v1.2.3", "dataiku-headless--v1.2.3"):
                assert git(remote, "rev-parse", f"{tag}^{{commit}}") == sha


@pytest.mark.parametrize(
    "branch",
    ["main", "release/0.7", "release/0.7.0", "release/0.6.9", "release/0.8.0;echo bad"],
)
def test_reject_invalid_or_already_finalized_target(branch):
    with pytest.raises(ValueError):
        HELPERS["validate_target_branch"](branch, "0.7.0")


@pytest.mark.parametrize(
    "branch,repo,accepted",
    [
        ("release/0.8.0", "org/project", True),
        ("release/0.7.1", "org/project", True),
        ("feat/tool", "org/project", False),
        ("release/0.8.0", "fork/project", False),
        ("release/0.8", "org/project", False),
    ],
)
def test_main_source_policy(branch, repo, accepted):
    workflow = yaml.safe_load(
        (ROOT / ".github/workflows/release-source.yml").read_text()
    )
    script = workflow["jobs"]["release-source"]["steps"][0]["run"]
    result = subprocess.run(
        ["bash", "-c", script],
        capture_output=True,
        text=True,
        env=dict(
            os.environ, HEAD_BRANCH=branch, HEAD_REPO=repo, BASE_REPO="org/project"
        ),
    )
    assert (result.returncode == 0) == accepted


@pytest.mark.parametrize("mode", ["merge", "squash", "wrong-head", "main-only-change"])
def test_publish_requires_release_merge_commit(tmp_path, mode):
    (tmp_path / "baseline").write_text("baseline")
    init_repo(tmp_path)
    git(tmp_path, "switch", "-c", "release/0.8.0")
    (tmp_path / "feature").write_text("feature")
    git(tmp_path, "add", ".")
    git(tmp_path, "commit", "-m", "feat: release feature")
    release_head = git(tmp_path, "rev-parse", "HEAD")
    git(tmp_path, "switch", "main")
    if mode == "main-only-change":
        (tmp_path / "extra").write_text("not reviewed in release branch")
        git(tmp_path, "add", ".")
        git(tmp_path, "commit", "-m", "fix: main-only change")
    if mode == "squash":
        git(tmp_path, "merge", "--squash", "release/0.8.0")
        git(tmp_path, "commit", "-m", "bump: squash release")
    else:
        git(tmp_path, "merge", "--no-ff", "release/0.8.0", "-m", "Merge release")
    if mode == "wrong-head":
        release_head = git(tmp_path, "rev-parse", "HEAD^1")
    if mode == "merge":
        HELPERS["validate_merge"](tmp_path, release_head)
    else:
        with pytest.raises(ValueError):
            HELPERS["validate_merge"](tmp_path, release_head)


def test_workflows_cover_release_targets_and_trusted_source_check():
    for name in ("ci.yml", "pr-title.yml"):
        # BaseLoader preserves YAML's 'on' as a string rather than a boolean.
        workflow = yaml.load(
            (ROOT / ".github/workflows" / name).read_text(), Loader=yaml.BaseLoader
        )
        assert workflow["on"]["pull_request"]["branches"] == ["main", "release/*"]
    workflow = yaml.load(
        (ROOT / ".github/workflows/release-source.yml").read_text(),
        Loader=yaml.BaseLoader,
    )
    assert workflow["on"]["pull_request_target"]["branches"] == ["main"]
    assert workflow["permissions"] == {}
    assert all(
        "uses" not in step for step in workflow["jobs"]["release-source"]["steps"]
    )


def test_preparation_opens_pr_into_existing_release_without_writing_it(tmp_path):
    repo = tmp_path / "repo"
    repo.mkdir()
    (repo / "pyproject.toml").write_text('version = "0.7.0"\n')
    init_repo(repo)
    baseline = git(repo, "rev-parse", "HEAD")
    git(repo, "switch", "-c", "release/0.8.0")
    remote = tmp_path / "remote.git"
    subprocess.run(
        ["git", "init", "--bare", str(remote)], check=True, capture_output=True
    )
    git(repo, "remote", "add", "origin", str(remote))
    git(repo, "push", "origin", "main", "release/0.8.0")
    (repo / "pyproject.toml").write_text('version = "0.8.0"\n')
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    gh = bin_dir / "gh"
    gh.write_text(
        '#!/bin/sh\nprintf "%s\\n" "$@" > "$RUNNER_TEMP/gh-args"\necho "https://example.invalid/pr/1"\n'
    )
    gh.chmod(0o755)
    workflow = yaml.safe_load((ROOT / ".github/workflows/bump.yml").read_text())
    script = workflow["jobs"]["prepare"]["steps"][-1]["run"]
    result = subprocess.run(
        ["bash", "-c", script],
        cwd=repo,
        capture_output=True,
        text=True,
        env=dict(
            os.environ,
            RELEASE_BRANCH="release/0.8.0",
            RUNNER_TEMP=str(tmp_path),
            GITHUB_STEP_SUMMARY=str(tmp_path / "summary"),
            PATH=f"{bin_dir}:{os.environ['PATH']}",
        ),
    )
    assert result.returncode == 0, result.stderr
    assert git(remote, "rev-parse", "main") == baseline
    assert git(remote, "rev-parse", "release/0.8.0") == baseline
    assert git(remote, "rev-parse", "prepare-release/0.8.0") != baseline
    assert git(remote, "tag", "--list") == ""
    args = (tmp_path / "gh-args").read_text().splitlines()
    assert args[args.index("--base") + 1] == "release/0.8.0"

SHA-256: 6c2a241bc9d697f7e30e598d7ff293283e7ea58f21ebef7ced6637be9d2e8d79