Skryptowe klonowanie projektów za pomocą ScriptRunnera

Jira Server / Data Center

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).

Groovy
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.

Groovy
// 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.

Groovy
// 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.

Groovy
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.

Go to top