~rjarry/dlrepo

880c5ac4b5025acf327044bb84e1e8f4d2d0f5e4 — Julien Floret 6 months ago 0014091
tag: decorrelate stable and released statuses

Before this patch, an URL referencing the "stable" tag was automatically
redirected to the released tag whose creation date is the most recent.
However, the latest released tag does not necessarily mean it is stable.
It could be a debug version, or a prototype... On the other hand, we may
want to set the next stable version internally before actually releasing
it.

This patchs decorrelates the "released" and "stable" statuses. The
"stable" status can now be set on a tag via the HTTP PUT method, whether
it is released or not, and an URL referencing the "stable" tag is
redirected to the latest stable tag.
The "stable" link in the HTML templates is changed to use the "stable"
status.

Signed-off-by: Julien Floret <julien.floret@6wind.com>
Acked-by: Thomas Faivre <thomas.faivre@6wind.com>
M dlrepo-cli => dlrepo-cli +21 -0
@@ 368,6 368,7 @@ def status(args):
    else:
        print("URL: %s" % client.make_url(url))
        print("released=%s" % data["tag"]["released"])
        print("stable=%s" % data["tag"]["stable"])
        print("locked=%s" % data["tag"]["locked"])
        print("publish_status=%s" % data["tag"].get("publish_status", ""))



@@ 399,6 400,26 @@ def release(args):
@sub_command(
    Arg("branch", metavar="BRANCH", help="the branch name"),
    Arg("tag", metavar="TAG", help="the tag name"),
    Arg(
        "-u",
        "--unset",
        action="store_true",
        help="unset the 'stable' status from the tag instead of setting it",
    ),
)
def stable(args):
    """
    Set or unset the 'stable' status on a tag.
    """
    client = HttpClient(args.url)
    url = os.path.join("branches", args.branch, args.tag) + "/"
    client.put(url, {"tag": {"stable": not args.unset}})


# --------------------------------------------------------------------------------------
@sub_command(
    Arg("branch", metavar="BRANCH", help="the branch name"),
    Arg("tag", metavar="TAG", help="the tag name"),
    Arg("job", metavar="JOB", help="the job name"),
    Arg(
        "-u",

M dlrepo/fs/branch.py => dlrepo/fs/branch.py +1 -1
@@ 42,7 42,7 @@ class Branch(SubDir):
            for t in tags:
                if name == "latest":
                    return t
                if t.is_released():
                if t.is_stable():
                    return t
            raise FileNotFoundError(name)
        return Tag(self, name)

M dlrepo/fs/product.py => dlrepo/fs/product.py +8 -1
@@ 71,7 71,7 @@ class ProductBranch(SubDir):
            for v in versions:
                if name == "latest":
                    return v
                if v.is_released():
                if v.is_stable():
                    return v
            raise FileNotFoundError(name)
        return Version(self, name)


@@ 122,6 122,13 @@ class Version(SubDir):
                return True
        return False

    def is_stable(self) -> bool:
        for fmt in self.get_formats():
            stable_path = fmt.path().resolve().parent / ".stable"
            if stable_path.is_file():
                return True
        return False

    def get_formats(self) -> Iterator[ArtifactFormat]:
        yield from ArtifactFormat.all(self)


M dlrepo/fs/tag.py => dlrepo/fs/tag.py +15 -0
@@ 74,6 74,21 @@ class Tag(SubDir):
        task = loop.create_task(self.do_release(released, semaphore))
        task.add_done_callback(self.done_cb)

    def _stable_path(self) -> Path:
        return self._path / ".stable"

    def is_stable(self) -> bool:
        return self._stable_path().is_file()

    def set_stable(self, stable: bool):
        if not self._path.is_dir():
            raise FileNotFoundError()
        path = self._stable_path()
        if stable:
            path.touch()
        elif path.is_file():
            path.unlink()

    def done_cb(self, task):
        if task.cancelled():
            return

M dlrepo/templates/branch.html => dlrepo/templates/branch.html +1 -1
@@ 17,7 17,7 @@
    <a href="latest/" class="tag alias">latest</a>
  </div>
  {% endif %}
  {% if tags|selectattr("released")|list %}
  {% if tags|selectattr("stable")|list %}
  <div>
    <a href="stable/" class="tag alias">stable</a>
  </div>

M dlrepo/templates/product_branch.html => dlrepo/templates/product_branch.html +1 -1
@@ 17,7 17,7 @@
    <a href="latest/" class="version alias">latest</a>
  </div>
  {% endif %}
  {% if versions|selectattr("released")|list %}
  {% if versions|selectattr("stable")|list %}
  <div>
    <a href="stable/" class="version alias">stable</a>
  </div>

M dlrepo/templates/tag.html => dlrepo/templates/tag.html +4 -1
@@ 8,11 8,14 @@
{% endblock %}

{% block page_content %}
{% if tag.released or tag.locked or tag.publish_status  %}
{% if tag.released or tag.locked or tag.publish_status or tag.stable %}
<section class="tag-status">
  {% if tag.released %}
  <span class="badge released">released</span>
  {% endif %}
  {% if tag.stable %}
  <span class="badge released">stable</span>
  {% endif %}
  {% if tag.locked %}
  <span class="badge locked">locked</span>
  {% endif %}

M dlrepo/views/branch.py => dlrepo/views/branch.py +1 -0
@@ 87,6 87,7 @@ class BranchView(BaseView):
                    "timestamp": t.timestamp,
                    "released": t.is_released(),
                    "locked": t.is_locked(),
                    "stable": t.is_stable(),
                    "publish_status": t.publish_status(),
                }
            )

M dlrepo/views/product.py => dlrepo/views/product.py +1 -0
@@ 146,6 146,7 @@ class ProductBranchView(BaseView):
                        "timestamp": v.timestamp,
                        "locked": v.is_locked(),
                        "released": v.is_released(),
                        "stable": v.is_stable(),
                    }
                )
        data = {

M dlrepo/views/tag.py => dlrepo/views/tag.py +7 -1
@@ 49,6 49,7 @@ class TagView(BaseView):
                "name": tag.name,
                "released": tag.is_released(),
                "locked": tag.is_locked(),
                "stable": tag.is_stable(),
                "publish_status": tag.publish_status(),
                "jobs": [],
            },


@@ 71,7 72,7 @@ class TagView(BaseView):

    async def put(self):
        """
        Change the released and/or locked status of a tag.
        Change the released, stable and/or locked statuses of a tag.
        """
        tag = self._get_tag()
        try:


@@ 82,6 83,9 @@ class TagView(BaseView):
            locked = data.get("locked")
            if locked is not None and not isinstance(locked, bool):
                raise TypeError()
            stable = data.get("stable")
            if stable is not None and not isinstance(stable, bool):
                raise TypeError()
        except (TypeError, KeyError) as e:
            raise web.HTTPBadRequest(reason="invalid parameters") from e



@@ 91,6 95,8 @@ class TagView(BaseView):
                tag.set_released(released, semaphore)
            if locked is not None:
                tag.set_locked(locked)
            if stable is not None:
                tag.set_stable(stable)
        except FileNotFoundError as e:
            raise web.HTTPNotFound() from e