1. Klonowanie projektów w trybie graficznym
Jedną z funkcjonalności ScriptRunnera jest moduł klonowania obiektów z graficznym interfejsem. Po przejściu do sekcji Built-in Scripts (Administration -> ScriptRunner -> Build-in Scripts) moduł ten można odnaleźć pod nazwą Copy project.

Po wywołaniu ekranu konfiguracyjnego użytkownik ma możliwość zdefiniowania, na podstawie którego projektu ma zostać utworzony klon i określić następujące parametry.
- Klucz klonowanego projektu.
- Nazwa klonowanego projektu.
- Czy mają zostać sklonowane wersje, komponenty, zgłoszenia i przypisane do projektu dashboardy oraz filtry.

2. Skryptowe klonowanie projektów
Przedstawiony w punkcie 1. sposób umożliwia ręczne klonowanie projektów w trybie „ad hoc”. Bardzo często oczekiwania administratorów są nieco bardziej złożone np.
- Klonowanie projektu po wykonaniu przejścia w postfunkcji.
- Założenie, że każdy nowy projekt musi być klonem wybranego projektu wzorcowego (projekt szablonowy).
- Modyfikacja danych projektu takich jak lider, uprawnione osoby, opis itp.
Wszystkie tego typu operacje wymagają stworzenia skryptu, który wykona operacje wyrażone w wymaganiach. W przypadku ScriptRunnera można wykorzystać mechanizm kopiowania projektów ukryty pod graficznym interfejsem i użyć go w samodzielnych skryptach Groovy’ego. Stwórzmy zatem przykładowy kod, który można uruchomić w konsoli oraz postfunkcji na dowolnym przejściu.
W celu skopiowania projektów należy posłużyć metodami wywołanymi na obiekcie klasy CopyProject, która jest dostarczana przez plugin ScriptRunner (com.onresolve.scriptrunner.canned.jira.admin.CopyProject). Zanim jednak przystąpimy do omówienia skryptu, należy odnieść się do wskazówki producenta, który zaleca uruchamianie procesu kopiowania w wątku (zob. SRJIRA-3793).
import com.atlassian.jira.util.thread.JiraThreadLocalUtils;
// Inicjalizacja wątku.
Thread executorThread = new Thread(JiraThreadLocalUtils.wrap() {
// Kod kopiowania projektu.
})
// Uruchomienie wątku.
executorThread.start();
Nie wchodząc w polemikę z producentem, czy jest to bug, czy feature, wyjaśnijmy na czym polega problem z uruchamianiem procesu kopiowania. Projekt składa się z wielu elementów takich jak komponenty, wersje, schematy i wreszcie zgłoszenia, których może być wiele. W trakcie klonowania każdy z tych elementów jest tworzony w osobnym procesie, a całość operacji trwa, w zależności od wielkości projektu, od kilkuset milisekund do nawet kilku sekund. Kluczowe znaczenie ma kolejność wykonywania operacji składowych, np. proces tworzenia zgłoszeń powinien czekać, aż projekt zostanie utworzony; tworzenie sub-tasków powinno wystartować po utworzeniu tasków itp. Jeżeli metoda kopiująca nie zostanie wywołana w wątku opakowanym w JiraThreadLocalUtils, to nie można mieć pewności, że operacje będą prawidłowo zsynchronizowane. Użycie zalecanego przez producenta sposobu uruchamiania spowoduje, że kolejne etapy tworzenia projektu będą właściwie uruchamiane, blokowane i nie dojdzie do systuacji losowego wykonywania wątków. W poniższym listingu przedstawiam przykładowy skrypt klonowania projektu uruchamiany zgodnie z zaleceniami producenta.
// Przykład z książki J. Kalinowski, Atlassian Jira Server & Data Center (Helion, Gliwice, 2023).
import com.atlassian.jira.util.thread.JiraThreadLocalUtils;
// 1. Pobranie obiektu typu interfejsowego ApplicationUser użytkownika z uprawnieniami
// do tworzenia projektów.
def adminUser = ComponentAccessor.getUserManager().getUserByName("admin");
// 2. Pobranie menedżerów.
def projectManager = ComponentAccessor.getProjectManager();
// 3. Dane do klonowania projektu.
//Klucz projektu będącego szablonem do sklonowania.
def sourceProjectKey = "TBP";
// Klucz i nazwa nowego projektu. Uwaga! Obie wartości muszą być unikatowe.
def newProjectKey = "BP1";
def newProjectName = "Business project - Processing sth";
// 4. Uruchomienie procesu klonowania projektu.
// 4.1. Zdefiniowanie wątku wykonującego operację i opakowanie go
// komponentem JiraThreadLocalUtils.
Thread executorThread = new Thread(JiraThreadLocalUtils.wrap() {
// 4.2. Utworzenie obiektu klasy kopiującej projekt.
def copyProject = new CopyProject();
// 4.3. Zdefiniowanie parametrów kopiowania, w tym klonowania wersji, komponentów,
// dashboardów, filtrów i zgłoszeń.
def copyProjectSettings = [
(CopyProject.FIELD_SOURCE_PROJECT) : sourceProjectKey,
(CopyProject.FIELD_TARGET_PROJECT) : newProjectKey,
(CopyProject.FIELD_TARGET_PROJECT_NAME) : newProjectName,
(CopyProject.FIELD_COPY_VERSIONS) : true,
(CopyProject.FIELD_COPY_COMPONENTS) : true,
(CopyProject.FIELD_COPY_ISSUES) : true,
(CopyProject.FIELD_COPY_DASH_AND_FILTERS) : true,
];
// 4.4. Walidacja parametrów klonowania.
def validateResult = copyProject.doValidate(copyProjectSettings, false);
if(validateResult.hasAnyErrors()) {
// 4.5. Zwrócenie logów błędów w przypadku niepoprawnej walidacji.
log.warn("Couldn't create project: $validateResult");
} else {
// 4.6. [OPCJA] Przelogowanie na użytkownika z grupy jira-administrators.
ComponentAccessor.getJiraAuthenticationContext().setLoggedInUser(adminUser);
// 4.7. Wykonanie klonowania projektu z zadanymi parametrami.
copyProject.doScript(copyProjectSettings);
}
});
// 4.8. Uruchomienie wątku.
executorThread.start();
Problem ten ma jeszcze większe znaczenie w postfunkcjach, szczególnie wtedy, gdy istnieje konieczność odwołania się do utworzonego projektu bezpośrednio po jego utworzeniu, np. użytkownik chce w postfunkcji utworzyć projekt i zmienić w nim description, project lead i in. W takim wypadku kody kopiowania i modyfikacji projektu należy uruchomić w osobnych wątkach i zsynchronizować je za pomocą metody Thread.join(), wywołanej na uruchomionym wątku. Schemat takiej operacji został przedstawiony w poniższym listingu.
// 1. Inicjalizacja pierwszego wątku.
Thread executorThread1 = new Thread(JiraThreadLocalUtils.wrap() {
// Kod klonowania projektu.
});
// 2. Inicjalizacja drugiego wątku.
Thread executorThread2 = new Thread(JiraThreadLocalUtils.wrap() {
// Kod modyfikacji projektu założonego w pierwszym wątku.
});
// 3. Uruchomienie i synchronizacja wątków.
// Drugi wątek będzie czekał na zakończenie pierwszego.
executorThread1.start();
executorThread1.join();
executorThread2.start();
executorThread2.join();
Poniżej przedstawiam przykładowy skrypt, który kopiuje projekt i modyfikuje niektóre jego parametry bezpośrednio po utworzeniu.
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.project.Project;
import com.onresolve.scriptrunner.canned.jira.admin.CopyProject;
import com.atlassian.jira.util.thread.JiraThreadLocalUtils;
import com.atlassian.jira.project.UpdateProjectParameters;
// ##### I. Kopiowanie projektu.
// 1. Pobranie obiektu typu interfejsowego ApplicationUser użytkownika z uprawnieniami do tworzenia projektów.
def adminUser = ComponentAccessor.getUserManager().getUserByName("admin");
// 2. Pobranie menedżerów.
def projectManager = ComponentAccessor.getProjectManager();
// 3. Dane do klonowania projektu.
//Klucz projektu będącego szablonem do sklonowania.
def sourceProjectKey = "TBP";
// Klucz i nazwa nowego projektu. Uwaga! Obie wartości muszą być unikatowe.
def newProjectKey = "BP1";
def newProjectName = "Business project - Processing sth";
// 4. Uruchomienie procesu klonowania projektu.
// 4.1. Zdefiniowanie wątku wykonującego operację i opakowanie go
// komponentem JiraThreadLocalUtils.
Thread executorThread1 = new Thread(JiraThreadLocalUtils.wrap() {
// 4.2. Utworzenie obiektu klasy kopiującej projekt.
def copyProject = new CopyProject();
// 4.3. Zdefiniowanie parametrów kopiowania, w tym klonowania wersji,
// komponentów, dashboardów, filtrów i zgłoszeń.
def copyProjectSettings = [
(CopyProject.FIELD_SOURCE_PROJECT) : sourceProjectKey,
(CopyProject.FIELD_TARGET_PROJECT) : newProjectKey,
(CopyProject.FIELD_TARGET_PROJECT_NAME) : newProjectName,
(CopyProject.FIELD_COPY_VERSIONS) : true,
(CopyProject.FIELD_COPY_COMPONENTS) : true,
(CopyProject.FIELD_COPY_ISSUES) : true,
(CopyProject.FIELD_COPY_DASH_AND_FILTERS) : true,
];
// 4.4. Walidacja parametrów klonowania.
def validateResult = copyProject.doValidate(copyProjectSettings, false);
if(validateResult.hasAnyErrors()) {
// 4.5. Zwrócenie logów błędów w przypadku niepoprawnej walidacji.
log.warn("Couldn't create project: ${validateResult}");
} else {
// 4.6. [OPCJA] Przelogowanie na użytkownika z grupy jira-administrators.
ComponentAccessor.getJiraAuthenticationContext().setLoggedInUser(adminUser);
// 4.7. Wykonanie klonowania projektu z zadanymi parametrami.
copyProject.doScript(copyProjectSettings);
}
});
// ##### II. Edycja skopiowanego projektu.
Thread executorThread2 = new Thread(JiraThreadLocalUtils.wrap() {
// 5. Pobranie obiektu nowo utworzonego projektu.
def newProjectObject = projectManager.getProjectObjByKey(newProjectKey);
// 6. Utworzenie zestawu parametrów dla edytowanego projektu.
def updateProjectParameters = UpdateProjectParameters
.forProject(newProjectObject.getId())
.description("Some new description") // Nowy opis projektu.
.leadUserKey("JIRAUSER10000"); // Klucz nowego lidera projektu.
// 7. Wykonanie zmiany w projekcie.
projectManager.updateProject(updateProjectParameters);
});
// 8. Uruchomienie i synchronizacja wątków.
executorThread1.start();
executorThread1.join();
executorThread2.start();
executorThread2.join();
Więcej informacji o klonowaniu i edycji projektów można znaleźć w mojej książce.