Projekt: git gradle spring-boot

http://git.e-strix.com/sample_fx_app.git/

W każdym (nawet freelancer’skim) projekcie przychodzi moment na to aby zacząć pisać kod i tu już dostajemy wiatr w żagiel i stopy wody pod kilem. W zasadzie ogranicza nas tylko lenistwo z jednej strony i kreatywność z drugiej.

Dla potrzeb prezentacji wybiorę sobie coś na czasie, a będzie to:

  • spring-boot
  • gradle
  • git
  • javafx

A zatem bez zbędnych patosów, teorii i literatury zacznijmy od stworzenia kilku plików.

Tworzenie projektu

Zacznijmy od utworzenia repozytorium. Jeśli nie widziałeś mojego wpisu na ten temat to zobacz pierw: php7-git-gitlist.

A teraz do rzeczy. Struktura projektu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
└───javafx-git-sample
    │     .gitignore
    │     build.gradle
    │     latest.properties
    │     settings.gradle
    │
    ├───gradle
    │        release.gradle
    │
    └───src
        └───main
            ├───java
            │    └───pl
            │        └───estrix
            │            └───javafx
            │                    FilterProp.java
            │                    JavaApplication.java
            │                    MainController.java
            │
            └───resources
                │     logo.png
                │     main.fxml
                │
                └───META-INF
                        spring.factories

Plik .gitignore

1
2
3
4
.gradle/
.idea/
build/
*.iml

Plik build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.1.RELEASE")
    }
}

apply from: 'gradle/release.gradle'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


bootJar {
    mainClassName = 'pl.estrix.javafx.JavaApplication'
    manifest {
        attributes 'Start-Class': 'pl.estrix.javafx.JavaApplication'
    }
}

springBoot {
    mainClassName = 'pl.estrix.javafx.JavaApplication'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    testCompile("junit:junit")
}


def buildTime() {
    new Date().format('yyyy.MM.dd HH:mm')
}

def generatedResources = "$buildDir/resources/main/"
sourceSets {
    main {
        output.dir(generatedResources, builtBy: 'generateMyResources')
    }
}

task generateMyResources {
    doLast {
        def generatedTmp = new File(generatedResources)
        if( !generatedTmp.exists() ) {
            generatedTmp.mkdirs()
        }
        def generated = new File(generatedResources, "filter.properties")

        generated.text = "estrix.application.biuld-time=${buildTime()}\n" +
                "estrix.application.name=${project.name}\n" +
                "estrix.application.version=${project.version}\n"
    }
}

Plik latest.properties

1
2
3
4
#Tue Apr 10 16:59:22 CEST 2018
bugfix=0
major=1
minor=0

Plik settings.gradle

1
rootProject.name = 'JavaFxGitSample'

Plik gradle/release.gradle

Notatka: Pomysł na zarządzanie wersjami pochodzi z https://www.youtube.com/watch?v=Y6SVoXFsw7I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Version {
    int major
    int minor
    int bugfix
    boolean snapshot = true

    static Version load(File versionFile){
        def props = new Properties()
        versionFile.withInputStream {
            stream -> props.load(stream)
        }
        def major = Integer.parseInt(props['major'])
        def minor = Integer.parseInt(props['minor'])
        def bugfix = Integer.parseInt(props['bugfix'])
        return new Version(major:major, minor:minor, bugfix:bugfix)
    }

    String toString(){
        "${major}.${minor}.${bugfix}${snapshot ? '-SNAPSHOT':''}"
    }

    void store(File versionFile){
        def props = new Properties()
        props['bugfix'] =  bugfix.toString()
        props['major']  =  major.toString()
        props['minor']  =  minor.toString()
        props.store(versionFile.newWriter(), null)
    }

    Version next(String releaseType = "bugfix"){
        if(releaseType == "major"){
            return new Version(major:major+1, minor:0, bugfix:0)
        }else if(releaseType == "minor"){
            return new Version(major:major, minor:minor+1, bugfix:0)
        }else{
            return new Version(major:major, minor:minor, bugfix:bugfix+1)
        }
    }
}

def latest = Version.load(rootProject.file("latest.properties"))
allprojects{
    version = latest.next()
}
gradle.taskGraph.whenReady { graph ->
    if(graph.hasTask(":release")){
        allprojects{
            version = latest.next(release.type)
            version.snapshot = false
        }
    }
}

task persistVersionInfo(type:Exec){
    commandLine "git" , "commit","-i", "latest.properties", "-m", "' update version to ${-> version }'"
    doFirst{
      version.store(file("latest.properties"))
   }
}

task tagWorkspace(type:Exec){  
   commandLine 'git', 'tag', "${-> version }"
   commandLine 'git', 'push', 'origin',  'HEAD:master'
}

task release(type:Release){
    type = System.getProperty("type")
    dependsOn  "build", "persistVersionInfo", "tagWorkspace"
}

import org.gradle.api.internal.tasks.options.Option

class Release extends DefaultTask{
    @Option(option="type", description="the type of a release.")
    String type = "bugfix"

    @TaskAction void release(){
        println "doing a $type release of ${project.version}"
    }
}

Plik src/main/java/pl/estrix/javafx/FilterProp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package pl.estrix.javafx;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties("filter.properties")
public class FilterProp {

    @Value("${estrix.application.name}")
    private String name;
    @Value("${estrix.application.version}")
    private String version;
    @Value("${estrix.application.biuld-time}")
    private String biuldTime;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getBiuldTime() {
        return biuldTime;
    }

    public void setBiuldTime(String biuldTime) {
        this.biuldTime = biuldTime;
    }
}

Plik src/main/java/pl/estrix/javafx/JavaApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package pl.estrix.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@ComponentScan("pl.estrix.javafx")
@PropertySource("classpath:filter.properties")
@Configuration
public class JavaApplication extends Application {

    private ConfigurableApplicationContext context;
    private Parent rootNode;

    public static void main(final String[] args) {
        Application.launch(args);
    }

    @Override
    public void init() throws Exception {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(JavaApplication.class);
        context = builder.run(getParameters().getRaw().toArray(new String[0]));

        FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
        loader.setControllerFactory(context::getBean);
        rootNode = loader.load();

    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(rootNode, 480, 320));
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        context.close();
    }

}

Plik src/main/java/pl/estrix/javafx/MainController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package pl.estrix.javafx;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MainController {

    @Autowired
    private FilterProp filterProp;

    @FXML
    private Label propName;
    @FXML
    private Label propVersion;
    @FXML
    private Label propBuildTime;

    @FXML
    public void initialize() {
        propName.setText(filterProp.getName());
        propVersion.setText(filterProp.getVersion());
        propBuildTime.setText(filterProp.getBiuldTime());
    }
}

Plik src/main/resources/main.fxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="320.0" prefWidth="480.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="pl.estrix.javafx.MainController">
   <children>
      <HBox alignment="CENTER" style="-fx-background-color: #ffffff;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <children>
            <AnchorPane prefHeight="320.0" prefWidth="255.0" HBox.hgrow="ALWAYS">
               <children>
                  <ImageView fitHeight="320.0" fitWidth="255.0" pickOnBounds="true" preserveRatio="true" AnchorPane.leftAnchor="0.0">
                     <image>
                        <Image url="@logo.png" />
                     </image>
                  </ImageView>
               </children>
            </AnchorPane>
            <VBox prefHeight="320.0" prefWidth="217.0" style="-fx-background-color: #ffffff;" HBox.hgrow="ALWAYS">
               <children>
                  <Label alignment="CENTER" contentDisplay="CENTER" maxWidth="1.7976931348623157E308" text="e-Strix.com" textAlignment="CENTER">
                     <VBox.margin>
                        <Insets bottom="10.0" top="10.0" />
                     </VBox.margin>
                  </Label>
                  <GridPane>
                    <columnConstraints>
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="106.0" minWidth="10.0" prefWidth="58.0" />
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="163.0" minWidth="10.0" prefWidth="163.0" />
                    </columnConstraints>
                    <rowConstraints>
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                     <children>
                        <Label text="Name" />
                        <Label text="Version" GridPane.rowIndex="1" />
                        <Label text="Buil Time" GridPane.rowIndex="2" />
                        <Label fx:id="propName" text="Label" GridPane.columnIndex="1" />
                        <Label fx:id="propVersion" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="1" />
                        <Label fx:id="propBuildTime" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2" />
                     </children>
                  </GridPane>
               </children>
            </VBox>
         </children>
         <cursor>
            <Cursor fx:constant="SW_RESIZE" />
         </cursor>
      </HBox>
   </children>
</AnchorPane>

Plik src/main/resources/META-INF/spring.factories

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pl.estrix.javafx.JavaApplication

Plik src/main/resources/logo.png

1
wget http://e-strix.com/wp-content/uploads/2018/04/logo.png -P src/main/resources/

Budowa lokalna

Uruchamianie

1
gradle clean generateMyResources bootRun

Budowanie jar

1
gradle clean generateMyResources bootJar

Uruchamianie

1
java -jar build\libs\JavaFxGitSample-1.0.1-SNAPSHOT.jar

No dobra, teraz wiemy jak zbudować aplikację. Ale docelowo ma to robić Ci, zatem zabieramy się za konfigurację job’ów.

Konfiguracja Jenkins

Budowa zdalna

Jenkins new project

W polu: Repository URL

git@git.e-strix.com:repositories/javafx-git-sample.git

W polu: Repository browser

gitlist

W polu: Repository browser -> URL

git.e-strix.com

W polu: Budowanie -> Invoke Gradle -> Version

Gradle_4_6

W polu: Budowanie -> Tasks

clean generateMyResources bootJar

 

Podnoszenie wersji

Kroki takie jak w przypadku „Budowa zdalna” z tą różnicą że w:

W polu: Budowanie -> Tasks

clean release --type minor

Gdzie wartość –type przyjmuje wartości:

  • major
  • minor
  • bugfix – domyślny

Publikacja na Landing Page

W polu: Budowanie -> Execute Shell -> Command

mkdir -p release_lib

rm -f ./release_lib/*

cp ${WORKSPACE}/../javafx-git-sample-release/build/libs/*.jar ./release_lib/

rm -f /home/appuser/public_html/download/*.jar

cp ./release_lib/*.jar /home/appuser/public_html/download/

 

Zakończenie

Jak widzisz storzenie projektu jest dość proste. Mimo że musiałem kilka godzin spędzić na odnalezieniu odpowiedniej dla mojego projektu ścieżki rozwoju. Przetestowałem kilka rozwiązań, pluginów i stwierdziłem że jednak najlepsze rozwiązanie to będzie ręczne napisanie skryptu (mimo że zapożyczyłem rozwiązanie od mądrych ludzi z internetu).

Ciągła integracja

Wstęp

Normalnie żaden projekt informatyczny nie jest budowany przez wydelegowaną osobę, nie miało by to większego sensu, ale chętnie na taki etat bym aplikował. No ale niestety dzisiejsze system ciągłej integracji zabierają naprawdę fajną pracę.

Tak czy inaczej dziś przedstawiam jak uruchomić Jenkins’a, jako że opensource ma się dobrze to wybieram tę wersję. Oczywiście nie narzucam wybieranie tego konkretnego rozwiązania.  Równie dobrze możesz wybrać Travis’a, TeamCity, Hudson’a czy którykolwiek z systemów jaki uważasz. Ja wybieram Jenkins’a ponieważ każda firma z którą pracowałem miała go na pokładzie.

Zatem zacznijmy od początku, czyli Java:

  1. Java

    Pierwszym krokiem będzie dodanie repozytorium do naszego Ubuntu i pobranie z niego zawartości:

    add-apt-repository ppa:webupd8team/java
    apt-get update

    Teraz spokojnie możemy instalujemy:

    apt-get install oracle-java8-installer

    Po całym procesie nasza java powinna wskazywać poprawną wartość, czytaj 1.8.x. Zatem zapytajmy się jaką wersje właśnie zainstalowaliśmy:

    java -version

    i/lub kompilator:

    javac -version

    Dla pewności, możemy upewnić się że nie będzie zgrzytów na etapie zależności. Więc sprawdzamy alternatywy:

    update-alternatives --config java

    Wybieramy naszą instalację i wszystko teraz będzie działać. W kolejnym kroku dodamy naszą instalację do zmiennych środowiskowych:

    vim /etc/environment

    Dodaj na początku klucz=wartość na początku pliku:

    JAVA_HOME="/usr/lib/jvm/java-8-oracle"

    I aktualizujemy zmiany poprzez komendę source:

    source /etc/environment 
    

    Teraz już jest definitywny koniec działań z javą. Sprawdzamy czy działa zmienna środowiskowa:

    echo $JAVA_HOME

    Działa, więc zainstalujmy robotników od czarnej roboty:

  2. Maven

    Maven, solidny framework oparty na pluginach, buduje aplikacje dla banków, ubezpieczeń, portale i inne bardzo ważne rzeczy.
    Zatem pobieramy i rozpakowujemy:

    Notatka: Tę akcję wykonuje w katalogu „/tmp”, ale nie ma takie obowiązku. Pamiętaj że jesteś na prawach „root”, więc to zobowiązuje do czegoś.

    wget http://ftp.man.poznan.pl/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz
    
    tar zxvf apache-maven-3.5.3-bin.tar.gz

    Teraz przenieśmy do katalogu /opt/:

    mv apache-maven-3.5.3/ /opt/

    I dodajemy zmienne środowiskowe, podobnie jak miało to miejsce dla pkt.1.
    Otwieramy plik środowisko:

     vim /etc/environment

    i dodajemy naszą zmienną:

     M2_HOME="/opt/apache-maven-3.5.3"

    Teraz jeszcze na końcu zmiennej PATH należy dodać odwołanie do katalogu bin:

    :$M2_HOME/bin

    Teraz aktualizujemy ponownie środowisko:

    source /etc/environment

    i sprawdzamy co się wydarzyło:

    # mvn -v 
    Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-24T20:49:05+01:00) 
    Maven home: /opt/apache-maven-3.5.3 
    Java version: 1.8.0_161, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-8-oracle/jre 
    Default locale: en_US, platform encoding: UTF-8 
    OS name: "linux", version: "4.4.0-92-generic", arch: "amd64", family: "unix"
  3. Gradle

    Kolejnym specjalistom do spraw budowy jest Gradle, projekt bardzo szybko się rozwija i niesie ze sobą duży potencjał. Pierwsza różnica jaka jest widoczna w stosunku do Mavena, to plik ustawień bazuje na formacie skryptów Groove, gdy poprzednika opisywaliśmy w XMLach.

    A zatem do dzieła. Pobieramy i rozpakowujemy:

    wget https://services.gradle.org/distributions/gradle-4.6-bin.zip
    unzip gradle-4.6-bin.zip

    Teraz przeniesiemy do katalogu /opt/, tak jak poprzednika:

    mv gradle-4.6/ /opt/

    I zmienne środowiskowe:

    vim /etc/environment

    Dodajemy wpis:

    GRADLE_HOME="/opt/gradle-4.6"

    i do zmiennej PATH, dodajemy odwołanie do katalogu bin:

    $GRADLE_HOME/bin

    Teraz aktualizacja środowiska:

    source /etc/environment

    i sprawdzamy efekt:

    # gradle -v 
    ------------------------------------------------------------ 
    Gradle 4.6 
    ------------------------------------------------------------ 
    Build time: 2018-02-28 13:36:36 UTC 
    Revision: 8fa6ce7945b640e6168488e4417f9bb96e4ab46c 
    Groovy: 2.4.12 
    Ant: Apache Ant(TM) version 1.9.9 compiled on February 2 2017 
    JVM: 1.8.0_161 (Oracle Corporation 25.161-b12) 
    OS: Linux 4.4.0-92-generic amd64
  4. Jenkins

    Całkiem spoko, jak do tej pory. Teraz czas na gwiazdę odcinka!

    Dodajemy repozytorium do nasze list w Ubuntu i aktualizujemy jak w przypadku Java’y:

    echo "deb https://pkg.jenkins.io/debian binary/" >> /etc/apt/sources.list
    apt-get update

    Teraz pora na główne zaklęcie:

    apt-get install jenkins

    Notatka: Instalator zapyta nas czy zainstalować aplikację oraz o to czy ma instalować bez sprawdzania poprawności. Ja należę do tych, którzy wierzą w oficjalną wersję zamieszczoną na oficjalnych stronach. W sumie ktoś by zauważył, gdyby coś niezdrowego się działo.

    Po instalacji może sprawdzić czy nasz serwis działa. Wykonaj komendę:

    service jenkins status

    Teraz czas na konfigurację:

    Przejdźmy na stronę:

    jenkins.e-strix.com:8080
    

    Widzimy że aplikacja działa. Super!
    Teraz mamy do wpisania klucz, o który prosi nas pierwszy formularz. Pobierzmy go zgodnie z instrukcją:

    cat /var/lib/jenkins/secrets/initialAdminPassword

    Teraz wybierzmy sobie opcje, które pluginy mają być dodane przy instalacji. Ja wybieram wersję domyślną. Do naszej pracy wystarczy ta opcja, ale o tym będzie dalej.

    Teraz sobie chwilę poczekajmy aż instalacja przejdzie do końca i pojawi się formularz w którym będziemy poproszeni o utworzenie pierwszego administratora.

    W sumie działa, zatem możemy na tym zakończyć… Ale kole mnie po oczach ten port w pasku przeglądarki. Zatem zróbmy sobie ProxyPass. O tym w kolejnej części o serwerze Apache.

     

  5. Apache

    W zasadzie możemy pominąć kilka pierwszysch kroków instalacji ponieważ poruszałem ten temat już w dwóch wpisach, na temat Subversion/SVN oraz o Git’cie. Ale przypomnę w dość szybki sposób.

    A zatem instalujemy:

    apt-get install apache2

    Włączamy potrzebne moduły:

    a2enmod rewrite
    a2enmod proxy
    a2enmod proxy_http
    a2enmod headers
    a2enmod proxy_html
    a2enmod xml2enc

    i restartujemy serwer Apache:

    service apache2 restart

    Ok, teraz przejdźmy do katalogu dostępnych stron:

    cd /etc/apache2/sites-available/

    Tworzymy katalog naszej strony:

    mkdir -p /var/www/jenkins.e-strix.com/public_html/

    i dalej konfiguracja naszej strony:

    vim jenkins.e-strix.com.conf

    i jej zawartość

    <VirtualHost *:80>
    
     ServerAdmin k.mucik@e-strix.pl
     DocumentRoot /var/www/jenkins.e-strx.com/public_html
    
     <Directory "/var/www/jenkins.e-strx.com/public_html/" >
      Options Indexes FollowSymLinks MultiViews
      AllowOverride All
      allow from all
      Require all granted
    
      DirectoryIndex index.html
     </Directory>
    
     ServerName jenkins.e-strix.com
    
     ProxyRequests Off
     AllowEncodedSlashes NoDecode
    
     ProxyPass / http://127.0.0.1:8080/ nocanon
     ProxyPassReverse / http://127.0.0.1:8080/
     ProxyPassReverse / http://jenkins.e-strix.com/
    
     ErrorLog /var/www/jenkins.e-strix.com/error.log
     CustomLog /var/www/jenkins.e-strix.com/access.log combined
    
    </VirtualHost>

    I gra gitara… Teraz dodajemy naszą stronę i przeładowywujemy ostawienia:

    a2ensite jenkins.e-strix.com
    service apache2 reload

    Podsumowanie

    Teraz mamy już prawie wszystko aby móc automatycznie budować i publikować projekty.

    Prawie? Czyli co jeszcze?

Odpowiedź jest dość prosta, brakuje nam projektów oraz miejsca gdzie będzie publikacja. O tych elementach będą kolejne wpisy. Projekty oczywiście będą korzystały z naszej dokonanej pracy.