Merge branch 'maintenance' into devel
Conflicts: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 src/octoprint/plugins/softwareupdate/updaters/update_script.py
This commit is contained in:
commit
efca776102
11 changed files with 395 additions and 95 deletions
|
|
@ -7,10 +7,10 @@
|
|||
# The file is processed from top to bottom, the first matching line wins. If <tag> or <reference commit> are left out,
|
||||
# the lookup table does not apply to the matched branches
|
||||
|
||||
# master, prerelease and rc shall not use the lookup table, only tags
|
||||
# master and rc shall not use the lookup table, only tags
|
||||
master
|
||||
rc/.*
|
||||
prerelease
|
||||
rc
|
||||
|
||||
# neither should disconnected checkouts, e.g. 'git checkout <tag>'
|
||||
HEAD
|
||||
|
|
|
|||
|
|
@ -299,14 +299,9 @@ There are three main branches in OctoPrint:
|
|||
|
||||
* `master`: The master branch always contains the current stable release. It
|
||||
is *only* updated on new releases. Will have a version number following
|
||||
the scheme `x.y.z` (e.g. `1.2.9`) or - if it's absolutely necessary to
|
||||
add a commit after release to this branch - `x.y.z.post<commits since x.y.z>`
|
||||
the scheme `<x>.<y>.<z>` (e.g. `1.2.9`) or - if it's absolutely necessary to
|
||||
add a commit after release to this branch - `<x>.<y>.<z>.post<commits since x.y.z>`
|
||||
(e.g. `1.2.9.post1`).
|
||||
* `prerelease`: This branch is only used during the short period where a
|
||||
future release has "graduated" from the `maintenance` branch and is already
|
||||
tagged, but still marked on Github as a pre-release. This is mostly used for
|
||||
update testing just before new releases. Version number follows the scheme
|
||||
`x.y.z` (e.g. `1.2.9`), just like the `master` branch.
|
||||
* `maintenance`: Improvements and fixes of the current release that make up
|
||||
the next release go here. More or less continously updated. You can consider
|
||||
this a preview of the next release version. It should be very stable at all
|
||||
|
|
@ -314,7 +309,7 @@ There are three main branches in OctoPrint:
|
|||
next stable release, so if you want to help out development, running the
|
||||
`maintenance` branch and reporting back anything you find is a very good way
|
||||
to do that. Will usually have a version number following the scheme
|
||||
`x.y.z+1.dev.<commits since increase of z>` for an OctoPrint version of `x.y.z`
|
||||
`<x>.<y>.<z+1>.dev<commits since increase of z>` for an OctoPrint version of `<x>.<y>.<z>`
|
||||
(e.g. `1.2.10.dev12`).
|
||||
* `devel`: Ongoing development of new features that will go into the next bigger
|
||||
release (MINOR version number increases) will happen on this branch. Usually
|
||||
|
|
@ -322,8 +317,16 @@ There are three main branches in OctoPrint:
|
|||
temporarily. Can be considered the "bleeding edge". All PRs should target
|
||||
*this* branch. Important improvements and fixes from PRs here are backported to
|
||||
`maintenance` as needed. Will usually have a version number following the
|
||||
scheme `x.y+1.0.dev<commits since increase of y>` for an OctoPrint version
|
||||
of `x.y.z` (e.g. `1.3.0.dev123`).
|
||||
scheme `<x>.<y+1>.0.dev<commits since increase of y>` for a current OctoPrint version
|
||||
of `<x>.<y>.<z>` (e.g. `1.3.0.dev123`).
|
||||
* `rc/maintenance`: This branch is reserved for future releases that have graduated from
|
||||
the `maintenance` branch and are now being pushed on the "Maintenance"
|
||||
pre release channel for further testing. Version number follows the scheme
|
||||
`<x>.<y>.<z>rc<n>` (e.g. `1.2.9rc1`).
|
||||
* `rc/devel`: This branch is reserved for future releases that have graduated from
|
||||
the `devel` branch and are now being pushed on the "Devel" pre release channel
|
||||
for further testing. Version number follows the scheme `<x>.<y+1>.0rc<n>` (e.g. `1.3.0rc1`)
|
||||
for a current stable OctoPrint version of `<x>.<y>.<z>`.
|
||||
|
||||
Additionally, from time to time you might see other branches pop up in the repository.
|
||||
Those usually have one of the following prefixes:
|
||||
|
|
@ -334,9 +337,6 @@ Those usually have one of the following prefixes:
|
|||
`maintenance` and `devel` branches.
|
||||
* `dev/...` or `feature/...`: New functionality under development that is to be merged
|
||||
into the `devel` branch.
|
||||
* `rc`: A branch similar in nature to the `prerelease` branch, only that it will be
|
||||
used to provide current release candidates of the next stable version to be derived
|
||||
from the `devel` branch.
|
||||
|
||||
There is also the `gh-pages` branch, which holds OctoPrint's web page, and a couple of
|
||||
older development branches that are slowly being migrated or deleted.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from . import version_checks, updaters, exceptions, util, cli
|
|||
|
||||
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server import admin_permission, VERSION, REVISION
|
||||
from octoprint.server import admin_permission, VERSION, REVISION, BRANCH
|
||||
from octoprint.util import dict_merge
|
||||
import octoprint.settings
|
||||
|
||||
|
|
@ -155,14 +155,18 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
#~~ SettingsPlugin API
|
||||
|
||||
def get_settings_defaults(self):
|
||||
update_script = os.path.join(self._basefolder, "scripts", "update-octoprint.py")
|
||||
return {
|
||||
"checks": {
|
||||
"octoprint": {
|
||||
"type": "github_release",
|
||||
"user": "foosel",
|
||||
"repo": "OctoPrint",
|
||||
"update_script": "{{python}} \"{update_script}\" --python=\"{{python}}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "update-octoprint.py")),
|
||||
"restart": "octoprint"
|
||||
"update_script": "{{python}} \"{update_script}\" --branch={{branch}} --force={{force}} \"{{folder}}\" {{target}}".format(update_script=update_script),
|
||||
"restart": "octoprint",
|
||||
"stable_branch": dict(branch="master", name="Stable"),
|
||||
"prerelease_branches": [dict(branch="rc/maintenance", name="Maintenance RCs"),
|
||||
dict(branch="rc/devel", name="Devel RCs")]
|
||||
},
|
||||
},
|
||||
"pip_command": None,
|
||||
|
|
@ -176,19 +180,47 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
if "checks" in data:
|
||||
del data["checks"]
|
||||
|
||||
if "check_providers" in data:
|
||||
del data["check_providers"]
|
||||
|
||||
checks = self._get_configured_checks()
|
||||
if "octoprint" in checks:
|
||||
data["octoprint_checkout_folder"] = self._get_octoprint_checkout_folder(checks=checks)
|
||||
data["octoprint_type"] = checks["octoprint"].get("type", None)
|
||||
|
||||
try:
|
||||
data["octoprint_method"] = self._get_update_method("octoprint", checks["octoprint"])
|
||||
except exceptions.UnknownUpdateType:
|
||||
data["octoprint_method"] = "unknown"
|
||||
|
||||
stable_branch = None
|
||||
prerelease_branches = []
|
||||
branch_mappings = []
|
||||
if "stable_branch" in checks["octoprint"]:
|
||||
branch_mappings.append(checks["octoprint"]["stable_branch"])
|
||||
stable_branch = checks["octoprint"]["stable_branch"]["branch"]
|
||||
if "prerelease_branches" in checks["octoprint"]:
|
||||
for mapping in checks["octoprint"]["prerelease_branches"]:
|
||||
branch_mappings.append(mapping)
|
||||
prerelease_branches.append(mapping["branch"])
|
||||
data["octoprint_branch_mappings"] = branch_mappings
|
||||
|
||||
data["octoprint_release_channel"] = stable_branch
|
||||
if checks["octoprint"].get("prerelease", False):
|
||||
channel = checks["octoprint"].get("prerelease_channel", BRANCH)
|
||||
if channel in prerelease_branches:
|
||||
data["octoprint_release_channel"] = channel
|
||||
|
||||
else:
|
||||
data["octoprint_checkout_folder"] = None
|
||||
data["octoprint_type"] = None
|
||||
data["octoprint_branch_mappings"] = []
|
||||
|
||||
return data
|
||||
|
||||
def on_settings_save(self, data):
|
||||
for key in self.get_settings_defaults():
|
||||
if key == "checks" or key == "cache_ttl" or key == "octoprint_checkout_folder" or key == "octoprint_type":
|
||||
if key in ("checks", "cache_ttl", "octoprint_checkout_folder", "octoprint_type", "octoprint_release_channel"):
|
||||
continue
|
||||
if key in data:
|
||||
self._settings.set([key], data[key])
|
||||
|
|
@ -203,6 +235,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
update_type = check.get("type", None)
|
||||
checkout_folder = check.get("checkout_folder", None)
|
||||
update_folder = check.get("update_folder", None)
|
||||
prerelease = check.get("prerelease", False)
|
||||
prerelease_channel = check.get("prerelease_channel", None)
|
||||
else:
|
||||
update_type = checkout_folder = update_folder = prerelease_channel = None
|
||||
prerelease = False
|
||||
|
||||
defaults = dict(
|
||||
plugins=dict(softwareupdate=dict(
|
||||
|
|
@ -210,7 +247,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
octoprint=dict(
|
||||
type=update_type,
|
||||
checkout_folder=checkout_folder,
|
||||
update_folder=update_folder
|
||||
update_folder=update_folder,
|
||||
prerelease=prerelease,
|
||||
prerelease_channel=prerelease_channel
|
||||
)
|
||||
)
|
||||
))
|
||||
|
|
@ -236,6 +275,17 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
pass
|
||||
self._version_cache_dirty = True
|
||||
|
||||
if "octoprint_release_channel" in data:
|
||||
prerelease_branches = self._settings.get(["checks", "octoprint", "prerelease_branches"])
|
||||
if prerelease_branches and data["octoprint_release_channel"] in [x["branch"] for x in prerelease_branches]:
|
||||
self._settings.set(["checks", "octoprint", "prerelease"], True, defaults=defaults, force=True)
|
||||
self._settings.set(["checks", "octoprint", "prerelease_channel"], data["octoprint_release_channel"], defaults=defaults, force=True)
|
||||
self._refresh_configured_checks = True
|
||||
else:
|
||||
self._settings.set(["checks", "octoprint", "prerelease"], False, defaults=defaults, force=True)
|
||||
self._settings.set(["checks", "octoprint", "prerelease_channel"], None, defaults=defaults, force=True)
|
||||
self._refresh_configured_checks = True
|
||||
|
||||
def get_settings_version(self):
|
||||
return 4
|
||||
|
||||
|
|
@ -368,7 +418,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
|
||||
try:
|
||||
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
|
||||
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current", information=information))
|
||||
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current",
|
||||
information=information))
|
||||
except exceptions.ConfigurationInvalid as e:
|
||||
flask.make_response("Update not properly configured, can't proceed: %s" % e.message, 500)
|
||||
|
||||
|
|
@ -621,6 +672,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
# one of our updates requires a restart of either type "octoprint" or "environment". Let's see if
|
||||
# we can actually perform that
|
||||
|
||||
restart_command = None
|
||||
if restart_type == "octoprint":
|
||||
restart_command = self._settings.global_get(["server", "commands", "serverRestartCommand"])
|
||||
elif restart_type == "environment":
|
||||
|
|
@ -729,18 +781,70 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
result["displayName"] = check.get("displayName", gettext("OctoPrint"))
|
||||
result["displayVersion"] = check.get("displayVersion", "{octoprint_version}")
|
||||
|
||||
stable_branch = "master"
|
||||
release_branches = []
|
||||
if "stable_branch" in check:
|
||||
release_branches.append(check["stable_branch"]["branch"])
|
||||
stable_branch = check["stable_branch"]["branch"]
|
||||
if "prerelease_branches" in check:
|
||||
release_branches += [x["branch"] for x in check["prerelease_branches"]]
|
||||
result["released_version"] = not release_branches or BRANCH in release_branches
|
||||
|
||||
if check["type"] == "github_commit":
|
||||
result["current"] = REVISION if REVISION else "unknown"
|
||||
else:
|
||||
result["current"] = VERSION
|
||||
|
||||
if check["type"] == "github_release" and (check["prerelease"] or BRANCH != stable_branch):
|
||||
# we are tracking github releases and are either also tracking prerelease OR are currently installed
|
||||
# from something that is not the stable (master) branch => we need to change some parameters
|
||||
|
||||
# we compare versions fully, not just the base so that we see a difference
|
||||
# between RCs + stable for the same version release
|
||||
result["force_base"] = False
|
||||
|
||||
if check.get("update_script", None):
|
||||
# if we are using the update_script, we need to set our update_branch and force
|
||||
# to install the exact version we requested
|
||||
|
||||
if check["prerelease"]:
|
||||
# we are tracking prereleases => we want to be on the correct prerelease channel/branch
|
||||
channel = check.get("prerelease_channel", None)
|
||||
if channel:
|
||||
# if we have a release channel, we also set our update_branch here to our release channel
|
||||
# in case it's not already set
|
||||
result["update_branch"] = check.get("update_branch", channel)
|
||||
|
||||
# we also force our target version in the update
|
||||
result["force_exact_version"] = True
|
||||
|
||||
else:
|
||||
# we are not tracking prereleases, but aren't on the stable branch either => switch back
|
||||
# to stable branch on update
|
||||
result["update_branch"] = check.get("update_branch", stable_branch)
|
||||
|
||||
|
||||
if BRANCH != result.get("prerelease_channel"):
|
||||
# we force python unequality check here because that will also allow us to
|
||||
# downgrade on a prerelease channel change (rc/devel => rc/maintenance)
|
||||
#
|
||||
# we detect channel changes by comparing the current branch with the target
|
||||
# branch of the release channel - unequality means we might have to handle
|
||||
# a downgrade
|
||||
result["release_compare"] = "python_unequal"
|
||||
|
||||
else:
|
||||
result["displayName"] = check.get("displayName", target)
|
||||
result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown"))
|
||||
if check["type"] in ("github_commit"):
|
||||
if check["type"] in ("github_commit",):
|
||||
result["current"] = check.get("current", None)
|
||||
else:
|
||||
result["current"] = check.get("current", check.get("displayVersion", None))
|
||||
|
||||
if "pip" in result:
|
||||
if not "pip_command" in check and self._settings.get(["pip_command"]) is not None:
|
||||
result["pip_command"] = self._settings.get(["pip_command"])
|
||||
|
||||
return result
|
||||
|
||||
def _log(self, lines, prefix=None, stream=None, strip=True):
|
||||
|
|
@ -777,22 +881,45 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
else:
|
||||
raise exceptions.UnknownCheckType()
|
||||
|
||||
def _get_update_method(self, target, check, valid_methods=None):
|
||||
"""
|
||||
Determines the update method for the given target and check.
|
||||
|
||||
If ``valid_methods`` is provided, determine method must be contained
|
||||
therein to be considered valid.
|
||||
|
||||
Raises an ``UnknownUpdateType`` exception if method cannot be determined
|
||||
or validated.
|
||||
"""
|
||||
|
||||
method = None
|
||||
if "method" in check:
|
||||
method = check["method"]
|
||||
else:
|
||||
if "update_script" in check:
|
||||
method = "update_script"
|
||||
elif "pip" in check:
|
||||
method = "pip"
|
||||
elif "python_updater" in check:
|
||||
method = "python_updated"
|
||||
|
||||
if method is None or (valid_methods and not method in valid_methods):
|
||||
raise exceptions.UnknownUpdateType()
|
||||
|
||||
return method
|
||||
|
||||
def _get_updater(self, target, check):
|
||||
"""
|
||||
Retrieves the updater for the given target and check configuration. Will raise an UnknownUpdateType if updater
|
||||
cannot be determined.
|
||||
"""
|
||||
|
||||
if "update_script" in check:
|
||||
return updaters.update_script
|
||||
elif "pip" in check:
|
||||
if not "pip_command" in check and self._settings.get(["pip_command"]) is not None:
|
||||
check["pip_command"] = self._settings.get(["pip_command"])
|
||||
return updaters.pip
|
||||
elif "python_updater" in check:
|
||||
return updaters.python_updater
|
||||
else:
|
||||
raise exceptions.UnknownUpdateType()
|
||||
mapping = dict(update_script=updaters.update_script,
|
||||
pip=updaters.pip,
|
||||
python_updater=updaters.python_updater)
|
||||
|
||||
method = self._get_update_method(target, check, valid_methods=mapping.keys())
|
||||
return mapping[method]
|
||||
|
||||
def _get_octoprint_checkout_folder(self, checks=None):
|
||||
if checks is None:
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ def _to_error(*lines):
|
|||
return u"".join(map(lambda x: _to_unicode(x, errors="replace"), lines))
|
||||
|
||||
|
||||
def update_source(git_executable, folder, target, force=False):
|
||||
def _rescue_changes(git_executable, folder):
|
||||
print(">>> Running: git diff --shortstat")
|
||||
returncode, stdout, stderr = _git(["diff", "--shortstat"], folder, git_executable=git_executable)
|
||||
if returncode != 0:
|
||||
|
|
@ -155,6 +155,13 @@ def update_source(git_executable, folder, target, force=False):
|
|||
for line in stdout:
|
||||
f.write(line)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def update_source(git_executable, folder, target, force=False, branch=None):
|
||||
if _rescue_changes(git_executable, folder):
|
||||
print(">>> Running: git reset --hard")
|
||||
returncode, stdout, stderr = _git(["reset", "--hard"], folder, git_executable=git_executable)
|
||||
if returncode != 0:
|
||||
|
|
@ -165,6 +172,18 @@ def update_source(git_executable, folder, target, force=False):
|
|||
if returncode != 0:
|
||||
raise RuntimeError("Could not update, \"git clean -f\" failed with returcode %d: %s" % (returncode, _to_error(*stdout)))
|
||||
|
||||
print(">>> Running: git fetch")
|
||||
returncode, stdout = _git(["fetch"], folder, git_executable=git_executable)
|
||||
if returncode != 0:
|
||||
raise RuntimeError("Could not update, \"git fetch\" failed with returncode %d: %s" % (returncode, stdout))
|
||||
print(stdout)
|
||||
|
||||
if branch is not None and branch.strip() != "":
|
||||
print(">>> Running: git checkout {}".format(branch))
|
||||
returncode, stdout = _git(["checkout", branch], folder, git_executable=git_executable)
|
||||
if returncode != 0:
|
||||
raise RuntimeError("Could not update, \"git checkout\" failed with returncode %d: %s" % (returncode, stdout))
|
||||
|
||||
print(">>> Running: git pull")
|
||||
returncode, stdout, stderr = _git(["pull"], folder, git_executable=git_executable)
|
||||
if returncode != 0:
|
||||
|
|
@ -199,18 +218,24 @@ def install_source(python_executable, folder, user=False, sudo=False):
|
|||
def parse_arguments():
|
||||
import argparse
|
||||
|
||||
boolean_trues = ["true", "yes", "1"]
|
||||
boolean_falses = ["false", "no", "0"]
|
||||
|
||||
parser = argparse.ArgumentParser(prog="update-octoprint.py")
|
||||
|
||||
parser.add_argument("--git", action="store", type=str, dest="git_executable",
|
||||
help="Specify git executable to use")
|
||||
parser.add_argument("--python", action="store", type=str, dest="python_executable",
|
||||
help="Specify python executable to use")
|
||||
parser.add_argument("--force", action="store_true", dest="force",
|
||||
help="Set this to force the update to only the specified version (nothing newer)")
|
||||
parser.add_argument("--force", action="store", type=lambda x: x in boolean_trues,
|
||||
dest="force", default=False,
|
||||
help="Set this to true to force the update to only the specified version (nothing newer, nothing older)")
|
||||
parser.add_argument("--sudo", action="store_true", dest="sudo",
|
||||
help="Install with sudo")
|
||||
parser.add_argument("--user", action="store_true", dest="user",
|
||||
help="Install to the user site directory instead of the general site directory")
|
||||
parser.add_argument("--branch", action="store", type=str, dest="branch", default=None,
|
||||
help="Specify the branch to make sure is checked out")
|
||||
parser.add_argument("folder", type=str,
|
||||
help="Specify the base folder of the OctoPrint installation to update")
|
||||
parser.add_argument("target", type=str,
|
||||
|
|
@ -238,13 +263,12 @@ def main():
|
|||
print("Python executable: {!r}".format(python_executable))
|
||||
|
||||
folder = args.folder
|
||||
target = args.target
|
||||
|
||||
import os
|
||||
if not os.access(folder, os.W_OK):
|
||||
raise RuntimeError("Could not update, base folder is not writable")
|
||||
|
||||
update_source(git_executable, folder, target, force=args.force)
|
||||
update_source(git_executable, folder, args.target, force=args.force, branch=args.branch)
|
||||
install_source(python_executable, folder, user=args.user, sudo=args.sudo)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -82,14 +82,14 @@ $(function() {
|
|||
self.config_cacheTtl = ko.observable();
|
||||
self.config_checkoutFolder = ko.observable();
|
||||
self.config_checkType = ko.observable();
|
||||
self.config_updateMethod = ko.observable();
|
||||
self.config_releaseChannel = ko.observable();
|
||||
|
||||
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
|
||||
self.confirmationDialog = $("#softwareupdate_confirmation_dialog");
|
||||
|
||||
self.config_availableCheckTypes = [
|
||||
{"key": "github_release", "name": gettext("Release")},
|
||||
{"key": "git_commit", "name": gettext("Commit")}
|
||||
];
|
||||
self.config_availableCheckTypes = ko.observableArray([]);
|
||||
self.config_availableReleaseChannels = ko.observableArray([]);
|
||||
|
||||
self.reloadOverlay = $("#reloadui_overlay");
|
||||
|
||||
|
|
@ -161,7 +161,8 @@ $(function() {
|
|||
softwareupdate: {
|
||||
cache_ttl: parseInt(self.config_cacheTtl()),
|
||||
octoprint_checkout_folder: self.config_checkoutFolder(),
|
||||
octoprint_type: self.config_checkType()
|
||||
octoprint_type: self.config_checkType(),
|
||||
octoprint_release_channel: self.config_releaseChannel()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -179,9 +180,28 @@ $(function() {
|
|||
};
|
||||
|
||||
self._copyConfig = function() {
|
||||
var updateMethod = self.settings.settings.plugins.softwareupdate.octoprint_method();
|
||||
|
||||
var availableCheckTypes = [];
|
||||
if (updateMethod == "update_script" || updateMethod == "python") {
|
||||
availableCheckTypes = [{"key": "github_release", "name": gettext("Release")},
|
||||
{"key": "git_commit", "name": gettext("Commit")}];
|
||||
} else {
|
||||
availableCheckTypes = [];
|
||||
}
|
||||
self.config_availableCheckTypes(availableCheckTypes);
|
||||
|
||||
var availableReleaseChannels = [];
|
||||
_.each(self.settings.settings.plugins.softwareupdate.octoprint_branch_mappings(), function(mapping) {
|
||||
availableReleaseChannels.push({"key": mapping.branch(), "name": gettext(mapping.name() || mapping.branch())});
|
||||
});
|
||||
self.config_availableReleaseChannels(availableReleaseChannels);
|
||||
|
||||
self.config_updateMethod(updateMethod);
|
||||
self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl());
|
||||
self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder());
|
||||
self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type());
|
||||
self.config_releaseChannel(self.settings.settings.plugins.softwareupdate.octoprint_release_channel());
|
||||
};
|
||||
|
||||
self._copyConfigBack = function() {
|
||||
|
|
@ -220,7 +240,7 @@ $(function() {
|
|||
var octoprint = data.information["octoprint"];
|
||||
if (octoprint && octoprint.hasOwnProperty("check")) {
|
||||
var check = octoprint.check;
|
||||
if (BRANCH != "master" && check["type"] == "github_release") {
|
||||
if (check["released_version"] === false && check["type"] == "github_release") {
|
||||
self.octoprintUnreleased(true);
|
||||
} else {
|
||||
self.octoprintUnreleased(false);
|
||||
|
|
@ -228,8 +248,8 @@ $(function() {
|
|||
|
||||
var checkoutFolder = (check["checkout_folder"] || "").trim();
|
||||
var updateFolder = (check["update_folder"] || "").trim();
|
||||
var checkType = check["type"] || "";
|
||||
if ((checkType == "github_release" || checkType == "git_commit") && checkoutFolder == "" && updateFolder == "") {
|
||||
var needsFolder = check["update_script"] || false;
|
||||
if (needsFolder && checkoutFolder == "" && updateFolder == "") {
|
||||
self.octoprintUnconfigured(true);
|
||||
} else {
|
||||
self.octoprintUnconfigured(false);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="control-group">
|
||||
<div class="control-group" data-bind="visible: config_updateMethod() == 'update_script' || config_updateMethod() == 'python_updater'">
|
||||
<label class="control-label">{{ _('OctoPrint checkout folder') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: config_checkoutFolder">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<div class="control-group" data-bind="visible: config_availableCheckTypes().length > 0 && config_checkType() == 'github_release'">
|
||||
<label class="control-label">{{ _('OctoPrint Release Channel') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="value: config_releaseChannel, options: config_availableReleaseChannels, optionsText: 'name', optionsValue: 'key'"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="control-group">
|
||||
<div class="control-group" data-bind="visible: config_availableCheckTypes().length > 0">
|
||||
<label class="control-label">{{ _('OctoPrint version tracking') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="value: config_checkType, options: config_availableCheckTypes, optionsText: 'name', optionsValue: 'key'"></select>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@
|
|||
<form class="form-horizontal">
|
||||
{% include "_snippets/plugins/softwareupdate/checkoutFolder.jinja2" %}
|
||||
{% include "_snippets/plugins/softwareupdate/versionTracking.jinja2" %}
|
||||
{% include "_snippets/plugins/softwareupdate/releaseChannel.jinja2" %}
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Version cache TTL') }}</label>
|
||||
<div class="controls">
|
||||
|
|
|
|||
|
|
@ -57,9 +57,11 @@ def perform_update(target, check, target_version, log_cb=None):
|
|||
raise ConfigurationInvalid("checkout_folder and update_folder are missing for update target %s, one is needed" % target)
|
||||
|
||||
update_script = check["update_script"]
|
||||
folder = check["update_folder"] if "update_folder" in check else check["checkout_folder"]
|
||||
pre_update_script = check["pre_update_script"] if "pre_update_script" in check else None
|
||||
post_update_script = check["post_update_script"] if "post_update_script" in check else None
|
||||
update_branch = check.get("update_branch", "")
|
||||
force_exact_version = check.get("force_exact_version", False)
|
||||
folder = check.get("update_folder", check["checkout_folder"])
|
||||
pre_update_script = check.get("pre_update_script", None)
|
||||
post_update_script = check.get("post_update_script", None)
|
||||
|
||||
caller = _get_caller(log_cb=log_cb)
|
||||
|
||||
|
|
@ -75,7 +77,12 @@ def perform_update(target, check, target_version, log_cb=None):
|
|||
### update
|
||||
|
||||
try:
|
||||
update_command = update_script.format(python=sys.executable, folder=folder, target=target_version)
|
||||
update_command = update_script.format(python=sys.executable,
|
||||
folder=folder,
|
||||
target=target_version,
|
||||
branch=update_branch,
|
||||
force="true" if force_exact_version else "false")
|
||||
|
||||
logger.debug("Target %s, running update script: %s" % (target, update_command))
|
||||
|
||||
caller.checked_call(update_command, cwd=folder)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,69 @@ RELEASE_URL = "https://api.github.com/repos/{user}/{repo}/releases"
|
|||
|
||||
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_release")
|
||||
|
||||
def _get_latest_release(user, repo, include_prerelease=False):
|
||||
def _filter_out_latest(releases,
|
||||
sort_key=None,
|
||||
include_prerelease=False,
|
||||
prerelease_channel=None):
|
||||
"""
|
||||
Filters out the newest of all matching releases.
|
||||
|
||||
Tests:
|
||||
|
||||
>>> release_1_2_15 = dict(name="1.2.15", tag_name="1.2.15", html_url="some_url", published_at="2016-07-29T19:53:29Z", prerelease=False, draft=False, target_commitish="prerelease")
|
||||
>>> release_1_2_16rc1 = dict(name="1.2.16rc1", tag_name="1.2.16rc1", html_url="some_url", published_at="2016-08-29T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/maintenance")
|
||||
>>> release_1_2_16rc2 = dict(name="1.2.16rc2", tag_name="1.2.16rc2", html_url="some_url", published_at="2016-08-30T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/maintenance")
|
||||
>>> release_1_2_17rc1 = dict(name="1.2.17rc1", tag_name="1.2.17rc1", html_url="some_url", published_at="2016-08-31T12:00:00Z", prerelease=True, draft=True, target_commitish="rc/maintenance")
|
||||
>>> release_1_3_0rc1 = dict(name="1.3.0rc1", tag_name="1.3.0rc1", html_url="some_url", published_at="2016-12-12T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/devel")
|
||||
>>> release_1_4_0rc1 = dict(name="1.4.0rc1", tag_name="1.4.0rc1", html_url="some_url", published_at="2017-12-12T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/future")
|
||||
>>> releases = [release_1_2_15, release_1_2_16rc1, release_1_2_16rc2, release_1_2_17rc1, release_1_3_0rc1, release_1_4_0rc1]
|
||||
>>> _filter_out_latest(releases, include_prerelease=False, prerelease_channel=None)
|
||||
('1.2.15', '1.2.15', 'some_url')
|
||||
>>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/maintenance")
|
||||
('1.2.16rc2', '1.2.16rc2', 'some_url')
|
||||
>>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/devel")
|
||||
('1.3.0rc1', '1.3.0rc1', 'some_url')
|
||||
>>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel=None)
|
||||
('1.4.0rc1', '1.4.0rc1', 'some_url')
|
||||
>>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/doesntexist")
|
||||
('1.2.15', '1.2.15', 'some_url')
|
||||
>>> _filter_out_latest([release_1_2_17rc1])
|
||||
(None, None, None)
|
||||
>>> _filter_out_latest([release_1_2_16rc1, release_1_2_16rc2])
|
||||
(None, None, None)
|
||||
"""
|
||||
|
||||
nothing = None, None, None
|
||||
|
||||
if sort_key is None:
|
||||
sort_key = lambda release: release.get("published_at", None)
|
||||
|
||||
# filter out prereleases and drafts
|
||||
filter_function = lambda rel: not rel["prerelease"] and not rel["draft"]
|
||||
if include_prerelease:
|
||||
if prerelease_channel:
|
||||
filter_function = lambda rel: not rel["draft"] and (
|
||||
not rel["prerelease"] or rel["target_commitish"] == prerelease_channel)
|
||||
else:
|
||||
filter_function = lambda rel: not rel["draft"]
|
||||
|
||||
releases = filter(filter_function, releases)
|
||||
if not releases:
|
||||
return nothing
|
||||
|
||||
# sort by sort_key
|
||||
releases = sorted(releases, key=sort_key)
|
||||
|
||||
# latest release = last in list
|
||||
latest = releases[-1]
|
||||
|
||||
return latest["name"], latest["tag_name"], latest.get("html_url", None)
|
||||
|
||||
|
||||
def _get_latest_release(user, repo, compare_type,
|
||||
include_prerelease=False,
|
||||
prerelease_channel=None,
|
||||
force_base=True):
|
||||
nothing = None, None, None
|
||||
r = requests.get(RELEASE_URL.format(user=user, repo=repo))
|
||||
|
||||
|
|
@ -27,36 +89,55 @@ def _get_latest_release(user, repo, include_prerelease=False):
|
|||
releases = r.json()
|
||||
|
||||
# sanitize
|
||||
required_fields = {"name", "tag_name", "html_url", "draft", "prerelease", "published_at"}
|
||||
required_fields = {"name", "tag_name", "html_url", "draft", "prerelease", "published_at", "target_commitish"}
|
||||
releases = filter(lambda rel: set(rel.keys()) & required_fields == required_fields,
|
||||
releases)
|
||||
|
||||
# filter out prereleases and drafts
|
||||
if include_prerelease:
|
||||
releases = filter(lambda rel: not rel["draft"], releases)
|
||||
else:
|
||||
releases = filter(lambda rel: not rel["prerelease"] and not rel["draft"],
|
||||
releases)
|
||||
comparable_factory = _get_comparable_factory(compare_type,
|
||||
force_base=force_base)
|
||||
sort_key = lambda release: comparable_factory(_get_sanitized_version(release["tag_name"]))
|
||||
|
||||
if not releases:
|
||||
return nothing
|
||||
|
||||
# sort by date
|
||||
comp = lambda a, b: cmp(a.get("published_at", None), b["published_at"])
|
||||
releases = sorted(releases, cmp=comp)
|
||||
|
||||
# latest release = last in list
|
||||
latest = releases[-1]
|
||||
|
||||
return latest["name"], latest["tag_name"], latest.get("html_url", None)
|
||||
return _filter_out_latest(releases,
|
||||
sort_key=sort_key,
|
||||
include_prerelease=include_prerelease,
|
||||
prerelease_channel=prerelease_channel)
|
||||
|
||||
|
||||
def _get_sanitized_version(version_string):
|
||||
"""
|
||||
Removes "-..." prefix from version strings.
|
||||
|
||||
Tests:
|
||||
>>> _get_sanitized_version("1.2.15")
|
||||
'1.2.15'
|
||||
>>> _get_sanitized_version("1.2.15-dev12")
|
||||
'1.2.15'
|
||||
"""
|
||||
if "-" in version_string:
|
||||
version_string = version_string[:version_string.find("-")]
|
||||
return version_string
|
||||
|
||||
|
||||
def _get_base_from_version_tuple(version_tuple):
|
||||
"""
|
||||
Reduces version tuple to base version.
|
||||
|
||||
Tests:
|
||||
|
||||
>>> _get_base_from_version_tuple(("1", "2", "15"))
|
||||
('1', '2', '15')
|
||||
>>> _get_base_from_version_tuple(("1", "2", "15", "*", "dev12"))
|
||||
('1', '2', '15')
|
||||
"""
|
||||
|
||||
base_version = []
|
||||
for part in version_tuple:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
return tuple(base_version)
|
||||
|
||||
|
||||
def _get_comparable_version_pkg_resources(version_string, force_base=True):
|
||||
import pkg_resources
|
||||
|
||||
|
|
@ -65,12 +146,7 @@ def _get_comparable_version_pkg_resources(version_string, force_base=True):
|
|||
if force_base:
|
||||
if isinstance(version, tuple):
|
||||
# old setuptools
|
||||
base_version = []
|
||||
for part in version:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
version = tuple(base_version)
|
||||
version = _get_base_from_version_tuple(version)
|
||||
else:
|
||||
# new setuptools
|
||||
version = pkg_resources.parse_version(version.base_version)
|
||||
|
|
@ -90,32 +166,66 @@ def _get_comparable_version_semantic(version_string, force_base=True):
|
|||
return version
|
||||
|
||||
|
||||
def _get_sanitized_compare_type(compare_type, custom=None):
|
||||
if not compare_type in ("python", "python_unequal",
|
||||
"semantic", "semantic_unequal",
|
||||
"unequal", "custom") or compare_type == "custom" and custom is None:
|
||||
compare_type = "python"
|
||||
return compare_type
|
||||
|
||||
|
||||
def _get_comparable_factory(compare_type, force_base=True):
|
||||
if compare_type in ("python", "python_unequal"):
|
||||
return lambda version: _get_comparable_version_pkg_resources(version, force_base=force_base)
|
||||
elif compare_type in ("semantic", "semantic_unequal"):
|
||||
return lambda version: _get_comparable_version_semantic(version, force_base=force_base)
|
||||
else:
|
||||
return lambda version: version
|
||||
|
||||
|
||||
def _get_comparator(compare_type, custom=None):
|
||||
if compare_type in ("python", "semantic"):
|
||||
return lambda a, b: a >= b
|
||||
elif compare_type == "custom":
|
||||
return custom
|
||||
else:
|
||||
return lambda a, b: a == b
|
||||
|
||||
|
||||
def _is_current(release_information, compare_type, custom=None, force_base=True):
|
||||
"""
|
||||
Checks if the provided release information indicates the version being the most current one.
|
||||
|
||||
Tests:
|
||||
|
||||
>>> _is_current(dict(remote=dict(value=None))
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.15"), remote=dict(value="1.2.16")))
|
||||
False
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")))
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")), force_base=False)
|
||||
False
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False)
|
||||
True
|
||||
>>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False, compare_type="python_unequal")
|
||||
False
|
||||
|
||||
"""
|
||||
|
||||
if release_information["remote"]["value"] is None:
|
||||
return True
|
||||
|
||||
if not compare_type in ("python", "semantic", "unequal", "custom") or compare_type == "custom" and custom is None:
|
||||
compare_type = "python"
|
||||
compare_type = _get_sanitized_compare_type(compare_type, custom=custom)
|
||||
comparable_factory = _get_comparable_factory(compare_type, force_base=force_base)
|
||||
comparator = _get_comparator(compare_type, custom=custom)
|
||||
|
||||
sanitized_local = _get_sanitized_version(release_information["local"]["value"])
|
||||
sanitized_remote = _get_sanitized_version(release_information["remote"]["value"])
|
||||
|
||||
try:
|
||||
if compare_type == "python":
|
||||
local_version = _get_comparable_version_pkg_resources(sanitized_local, force_base=force_base)
|
||||
remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base)
|
||||
return local_version >= remote_version
|
||||
|
||||
elif compare_type == "semantic":
|
||||
local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base)
|
||||
remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base)
|
||||
return local_version >= remote_version
|
||||
|
||||
elif compare_type == "custom":
|
||||
return custom(sanitized_local, sanitized_remote)
|
||||
|
||||
else:
|
||||
return sanitized_local == sanitized_remote
|
||||
return comparator(comparable_factory(sanitized_local),
|
||||
comparable_factory(sanitized_remote))
|
||||
except:
|
||||
logger.exception("Could not check if version is current due to an error, assuming it is")
|
||||
return True
|
||||
|
|
@ -127,12 +237,17 @@ def get_latest(target, check, custom_compare=None):
|
|||
|
||||
current = check.get("current", None)
|
||||
include_prerelease = check.get("prerelease", False)
|
||||
prerelease_channel = check.get("prerelease_channel", None)
|
||||
force_base = check.get("force_base", True)
|
||||
compare_type = _get_sanitized_compare_type(check.get("release_compare", "python"),
|
||||
custom=custom_compare)
|
||||
|
||||
remote_name, remote_tag, release_notes = _get_latest_release(check["user"],
|
||||
check["repo"],
|
||||
include_prerelease=include_prerelease)
|
||||
compare_type = check["release_compare"] if "release_compare" in check else "python"
|
||||
compare_type,
|
||||
include_prerelease=include_prerelease,
|
||||
prerelease_channel=prerelease_channel,
|
||||
force_base=force_base)
|
||||
|
||||
information =dict(
|
||||
local=dict(name=current, value=current),
|
||||
|
|
|
|||
Loading…
Reference in a new issue