diff --git a/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js
new file mode 100644
index 00000000..f9438bdd
--- /dev/null
+++ b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js
@@ -0,0 +1,232 @@
+// This file is part of InvenioVCS
+// Copyright (C) 2023 CERN.
+//
+// Invenio VCS is free software; you can redistribute it and/or modify it
+// under the terms of the MIT License; see LICENSE file for more details.
+import $ from "jquery";
+
+function addResultMessage(element, color, icon, message) {
+ element.classList.remove("hidden");
+ element.classList.add(color);
+ element.querySelector(`.icon`).className = `${icon} small icon`;
+ element.querySelector(".content").textContent = message;
+}
+
+// function from https://www.w3schools.com/js/js_cookies.asp
+function getCookie(cname) {
+ let name = cname + "=";
+ let decodedCookie = decodeURIComponent(document.cookie);
+ let ca = decodedCookie.split(";");
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i];
+ while (c.charAt(0) == " ") {
+ c = c.substring(1);
+ }
+ if (c.indexOf(name) == 0) {
+ return c.substring(name.length, c.length);
+ }
+ }
+ return "";
+}
+
+const REQUEST_HEADERS = {
+ "Content-Type": "application/json",
+ "X-CSRFToken": getCookie("csrftoken"),
+};
+
+const sync_button = document.getElementById("sync_repos");
+if (sync_button) {
+ sync_button.addEventListener("click", function () {
+ const resultMessage = document.getElementById("sync-result-message");
+ const loaderIcon = document.getElementById("loader_icon");
+ const buttonTextElem = document.getElementById("sync_repos_btn_text");
+ const buttonText = buttonTextElem.innerHTML;
+ const loadingText = sync_button.dataset.loadingText;
+ const provider = sync_button.dataset.provider;
+
+ const url = `/api/user/vcs/${provider}/repositories/sync`;
+ const request = new Request(url, {
+ method: "POST",
+ headers: REQUEST_HEADERS,
+ });
+
+ buttonTextElem.innerHTML = loadingText;
+ loaderIcon.classList.add("loading");
+
+ function fetchWithTimeout(url, options, timeout = 100000) {
+ /** Timeout set to 100000 ms = 1m40s .*/
+ return Promise.race([
+ fetch(url, options),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('timeout')), timeout)
+ )
+ ]);
+ }
+
+ syncRepos(request);
+
+ async function syncRepos(request) {
+ try {
+ const response = await fetchWithTimeout(request);
+ loaderIcon.classList.remove("loading");
+ sync_button.classList.add("disabled");
+ buttonTextElem.innerHTML = buttonText;
+ if (response.ok) {
+ addResultMessage(
+ resultMessage,
+ "positive",
+ "checkmark",
+ "Repositories synced successfully. Please reload the page."
+ );
+ sync_button.classList.remove("disabled");
+ setTimeout(function () {
+ resultMessage.classList.add("hidden");
+ }, 10000);
+ } else {
+ addResultMessage(
+ resultMessage,
+ "negative",
+ "cancel",
+ `Request failed with status code: ${response.status}`
+ );
+ setTimeout(function () {
+ resultMessage.classList.add("hidden");
+ }, 10000);
+ sync_button.classList.remove("disabled");
+ }
+ } catch (error) {
+ loaderIcon.classList.remove("loading");
+ if(error.message === "timeout"){
+ addResultMessage(
+ resultMessage,
+ "warning",
+ "hourglass",
+ "This action seems to take some time, refresh the page after several minutes to inspect the synchronisation."
+ );
+ }
+ else {
+ addResultMessage(
+ resultMessage,
+ "negative",
+ "cancel",
+ `There has been a problem: ${error}`
+ );
+ setTimeout(function () {
+ resultMessage.classList.add("hidden");
+ }, 7000);
+ }
+ }
+ }
+ });
+}
+
+const repositories = document.getElementsByClassName("repository-item");
+if (repositories) {
+ for (const repo of repositories) {
+ repo.addEventListener("change", function (event) {
+ sendEnableDisableRequest(event.target.checked, repo);
+ });
+ }
+}
+
+function sendEnableDisableRequest(checked, repo) {
+ const input = repo.querySelector("input[data-repo-id]");
+ const repo_id= input.getAttribute("data-repo-id");
+ const provider = input.getAttribute("data-provider");
+ const switchMessage = repo.querySelector(".repo-switch-message");
+
+ let url;
+ if (checked === true) {
+ url = `/api/user/vcs/${provider}/repositories/${repo_id}/enable`;
+ } else if (checked === false) {
+ url = `/api/user/vcs/${provider}/repositories/${repo_id}/disable`;
+ }
+
+ const request = new Request(url, {
+ method: "POST",
+ headers: REQUEST_HEADERS,
+ });
+
+ sendRequest(request);
+
+ async function sendRequest(request) {
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ addResultMessage(
+ switchMessage,
+ "positive",
+ "checkmark",
+ "Repository synced successfully. Please reload the page."
+ );
+ setTimeout(function () {
+ switchMessage.classList.add("hidden");
+ }, 10000);
+ } else {
+ addResultMessage(
+ switchMessage,
+ "negative",
+ "cancel",
+ `Request failed with status code: ${response.status}`
+ );
+ setTimeout(function () {
+ switchMessage.classList.add("hidden");
+ }, 5000);
+ }
+ } catch (error) {
+ addResultMessage(
+ switchMessage,
+ "negative",
+ "cancel",
+ `There has been a problem: ${error}`
+ );
+ setTimeout(function () {
+ switchMessage.classList.add("hidden");
+ }, 7000);
+ }
+ }
+}
+
+// DOI badge modal
+$(".doi-badge-modal").modal({
+ selector: {
+ close: ".close.button",
+ },
+ onShow: function () {
+ const modalId = $(this).attr("id");
+ const $modalTrigger = $(`#${modalId}-trigger`);
+ $modalTrigger.attr("aria-expanded", true);
+ },
+ onHide: function () {
+ const modalId = $(this).attr("id");
+ const $modalTrigger = $(`#${modalId}-trigger`);
+ $modalTrigger.attr("aria-expanded", false);
+ },
+});
+
+$(".doi-modal-trigger").on("click", function (event) {
+ const modalId = $(event.target).attr("aria-controls");
+ $(`#${modalId}.doi-badge-modal`).modal("show");
+});
+
+$(".doi-modal-trigger").on("keydown", function (event) {
+ if (event.key === "Enter") {
+ const modalId = $(event.target).attr("aria-controls");
+ $(`#${modalId}.doi-badge-modal`).modal("show");
+ }
+});
+
+// ON OFF toggle a11y
+const $onOffToggle = $(".toggle.on-off");
+
+$onOffToggle &&
+ $onOffToggle.on("change", (event) => {
+ const target = $(event.target);
+ const $onOffToggleCheckedAriaLabel = target.data("checked-aria-label");
+ const $onOffToggleUnCheckedAriaLabel = target.data("unchecked-aria-label");
+ if (event.target.checked) {
+ target.attr("aria-label", $onOffToggleCheckedAriaLabel);
+ } else {
+ target.attr("aria-label", $onOffToggleUnCheckedAriaLabel);
+ }
+ });
diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py
new file mode 100644
index 00000000..c2cdf3ca
--- /dev/null
+++ b/invenio_vcs/ext.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# This file is part of Invenio.
+# Copyright (C) 2025 CERN.
+#
+# Invenio is free software; you can redistribute it and/or modify it
+# under the terms of the MIT License; see LICENSE file for more details.
+
+"""Invenio module that adds VCS integration to the platform."""
+
+from flask import current_app, request
+from flask_menu import current_menu
+from invenio_i18n import LazyString
+from invenio_i18n import gettext as _
+from invenio_theme.proxies import current_theme_icons
+from six import string_types
+from werkzeug.utils import cached_property, import_string
+
+from invenio_vcs.config import get_provider_list
+from invenio_vcs.receivers import VCSReceiver
+from invenio_vcs.service import VCSRelease
+from invenio_vcs.utils import obj_or_import_string
+
+from . import config
+
+
+class InvenioVCS(object):
+ """Invenio-VCS extension."""
+
+ def __init__(self, app=None):
+ """Extension initialization."""
+ if app:
+ self.init_app(app)
+
+ @cached_property
+ def release_api_class(self):
+ """Release API class."""
+ cls = current_app.config["VCS_RELEASE_CLASS"]
+ if isinstance(cls, string_types):
+ cls = import_string(cls)
+ assert issubclass(cls, VCSRelease)
+ return cls
+
+ @cached_property
+ def release_error_handlers(self):
+ """Release error handlers."""
+ error_handlers = current_app.config.get("VCS_ERROR_HANDLERS") or []
+ return [
+ (obj_or_import_string(error_cls), obj_or_import_string(handler))
+ for error_cls, handler in error_handlers
+ ]
+
+ def init_app(self, app):
+ """Flask application initialization."""
+ self.init_config(app)
+ app.extensions["invenio-vcs"] = self
+
+ def init_config(self, app):
+ """Initialize configuration."""
+ app.config.setdefault(
+ "VCS_SETTINGS_TEMPLATE",
+ app.config.get("SETTINGS_TEMPLATE", "invenio_vcs/settings/base.html"),
+ )
+
+ for k in dir(config):
+ if k.startswith("VCS_"):
+ app.config.setdefault(k, getattr(config, k))
+
+
+def finalize_app_ui(app):
+ """Finalize app."""
+ init_menu(app)
+ init_webhooks(app)
+
+
+def finalize_app_api(app):
+ """Finalize app."""
+ init_webhooks(app)
+
+
+def init_menu(app):
+ """Init menu."""
+ for provider in get_provider_list(app):
+
+ def is_active(current_node):
+ return (
+ request.endpoint.startswith("invenio_vcs.")
+ and request.view_args.get("provider", "") == current_node.name
+ )
+
+ current_menu.submenu(f"settings.{provider.id}").register(
+ endpoint="invenio_vcs.get_repositories",
+ endpoint_arguments_constructor=lambda id=provider.id: {"provider": id},
+ text=_(
+ "%(icon)s %(provider)s",
+ icon=LazyString(
+ lambda: f''
+ ),
+ provider=provider.name,
+ ),
+ order=10,
+ active_when=is_active,
+ )
+
+
+def init_webhooks(app):
+ """Register the webhook receivers based on the configured VCS providers."""
+ state = app.extensions.get("invenio-webhooks")
+ if state is not None:
+ for provider in get_provider_list(app):
+ # Procedurally register the webhook receivers instead of including them as an entry point, since
+ # they are defined in the VCS provider config list rather than in the instance's setup.cfg file.
+ if provider.id not in state.receivers:
+ state.register(provider.id, VCSReceiver)
diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html
new file mode 100644
index 00000000..da997bd2
--- /dev/null
+++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html
@@ -0,0 +1,55 @@
+{# -*- coding: utf-8 -*-
+
+ This file is part of Invenio.
+ Copyright (C) 2023 CERN.
+
+ Invenio is free software; you can redistribute it and/or modify it
+ under the terms of the MIT License; see LICENSE file for more details.
+#}
+{% from "semantic-ui/invenio_formatter/macros/badges.html" import badges_formats_list %}
+
+{%- macro doi_badge(doi, doi_url, provider_id, provider) %}
+ {%- block doi_badge scoped %}
+ {% set image_url = url_for('invenio_vcs_badge.index', provider=provider, repo_provider_id=provider_id, _external=True) %}
+
+
+
+
+
{{ _("DOI Badge") }}
+
+
+
+
+ {{ _("This badge points to the latest released version of your repository. If you want a DOI badge for a specific release, please follow the DOI link for one of the specific releases and grab badge from the archived record.") }}
+
+
+ {%- endblock %}
+{%- endmacro %}
diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html
new file mode 100644
index 00000000..05e7a12f
--- /dev/null
+++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html
@@ -0,0 +1,184 @@
+{# -*- coding: utf-8 -*-
+
+ This file is part of Invenio.
+ Copyright (C) 2023 CERN.
+
+ Invenio is free software; you can redistribute it and/or modify it
+ under the terms of the MIT License; see LICENSE file for more details.
+#}
+{%- import "invenio_vcs/settings/helpers.html" as helpers with context %}
+{%- if not request.is_xhr %}
+ {%- extends config.VCS_SETTINGS_TEMPLATE %}
+{%- endif %}
+
+{%- block settings_content %}
+ {%- if connected %}
+ {%- block repositories_get_started %}
+ {{
+ helpers.panel_start(
+ _('Repositories'),
+ icon=vocabulary["icon"] + " icon",
+ btn_text=_('Sync now'),
+ btn_loading_text=_('Syncing'),
+ btn_icon='sync alternate icon',
+ btn_id='sync_repos',
+ btn_name='sync-repos',
+ btn_help_text=_('(updated {})').format(last_sync|naturaltime),
+ provider=provider,
+ loaded_message_id='sync-result-message',
+ id="vcs-view",
+ )
+ }}
+
+
+
+
+
+ {{ _("Get started") }}
+
+
+
+
+
+
1 {{ _("Flip the switch") }}
+
+
+ {{ _('Select the repository you want to preserve, and toggle
+ the switch below to turn on automatic preservation of your software.') }}
+
+
+
+
+
+
+
+
+
+
+
+
2 {{ _("Create a release") }}
+
+
+ {{ _('Go to %(name)s and create a release . %(site_name)s will automatically download a .zip-ball of each new release and register a DOI.',
+ name=vocabulary["name"], site_name=config.THEME_SITENAME | default('System')) }}
+
+
+
+
+
3 {{ _("Get the badge") }}
+
+
+ {{ _('After your first release, a DOI badge that you can include in your %(name)s
+ README will appear next to your repository below.', name=vocabulary["name"]) }}
+
+
+
+ {{ _('If your organization\'s repositories do not show up in the list, please
+ ensure you have enabled third-party
+ access to the {} application. Private repositories are not supported.')
+ .format(config.THEME_SITENAME | default('Invenio')) }}
+
+
+ {%- endblock %}
+
+ {%- if not repos %}
+
+ {{ _('You have no repositories on %(name)s.', name=vocabulary["name"]) }}
+
+
+ {{_('Go to %(name)s and create your first or
+ click the Sync button to synchronize the latest changes from %(name)s.', name=vocabulary["name"], url=new_repo_url)}}
+
+ {%- else %}
+
+ {%- for repo_id, repo in repos.items() if not repo.instance or not repo.instance.hook %}
+
+ {% include "invenio_vcs/settings/index_item.html" with context %}
+
+ {%- endfor %}
+
+ {% endif %}
+
+ {{ helpers.panel_end() }}
+ {%- endblock %}
+
+ {#- If the user has not connected their VCS account... #}
+ {%- else %}
+ {%- block connect_to_vcs_account %}
+ {{ helpers.panel_start(_('%(name)s', name=vocabulary["name"]), icon=vocabulary["icon"] + " icon") }}
+
+ {{ _('To get started, click "Connect" and we will get a list of your %(repositories)s from %(name)s.', repositories=vocabulary["repository_name_plural"], name=vocabulary["name"]) }}
+
+
+
+ {{ helpers.panel_end() }}
+ {%- endblock %}
+ {%- endif %}
+{%- endblock %}
+
+{%- block javascript %}
+ {{ super() }}
+ {{ webpack['invenio-vcs-init.js'] }}
+{%- endblock javascript %}
diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html
new file mode 100644
index 00000000..72c2546b
--- /dev/null
+++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html
@@ -0,0 +1,59 @@
+{# -*- coding: utf-8 -*-
+
+ This file is part of Invenio.
+ Copyright (C) 2023 CERN.
+
+ Invenio is free software; you can redistribute it and/or modify it
+ under the terms of the MIT License; see LICENSE file for more details.
+#}
+{%- from "invenio_vcs/helpers.html" import doi_badge with context -%}
+{%- set release = repo.get('latest') %}
+
+{%- block repository_item %}
+
+ CITATION.cff {{ _('files are plain text files with human-
+ and machine-readable citation information for software. Code developers can include them in their repositories to let others know how to correctly cite their software.') }}
+
+
{{ _("An example of the CITATION.cff for this release can be found below:") }}
+
+
+cff-version: 1.1.0
+message: "If you use this software, please cite it as below."
+authors:
+- family-names: Joe
+given-names: Johnson
+orcid: https://orcid.org/0000-0000-0000-0000
+title: {%- if release.record %}{{ release.record.data["metadata"]["title"] }}{%- endif %}
+version: {{ release_tag }}
+date-released: {{ release.event.payload.get("release", {}).get("published_at", "")[:10] if release.event else '2021-07-28' }}
+
+ {% set active = false %}
+ {%- endblock release_details_tabs_content %}
+
+
+
+ {%- endblock release_details_content %}
+
+
+ {%- endblock release_header %}
+
+
+ {%- block release_footer scoped %}
+ {%- if not is_last %}{%- endif %}
+ {%- endblock release_footer %}
+
+{%- endblock %}
diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html
new file mode 100644
index 00000000..ce2df3f9
--- /dev/null
+++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html
@@ -0,0 +1,144 @@
+{# -*- coding: utf-8 -*-
+
+ This file is part of Invenio.
+ Copyright (C) 2023 CERN.
+
+ Invenio is free software; you can redistribute it and/or modify it
+ under the terms of the MIT License; see LICENSE file for more details.
+#}
+
+{%- import "invenio_vcs/settings/helpers.html" as helpers with context %}
+{%- from "invenio_vcs/helpers.html" import doi_badge with context -%}
+
+{%- extends config.VCS_SETTINGS_TEMPLATE %}
+
+{%- block settings_content %}
+ {% set active = true %}
+
+ {%- block repo_details_header scoped %}
+
+
+ {{ _('Go to {} and create a release. {}
+ will automatically download a .zip-ball of each new release and register a DOI.')
+ .format(vocabulary["name"], config.THEME_SITENAME | default('Invenio')) }}
+