Increased collaboration
Shared responsibility
Autonomous teams
Focus on the process, not just the product
Risk management
Resource exploitation
Principles inspire practices
Practices require tools
A system responsible for managing changes to the project files
De-facto reference distributed version control system
¹ Less difference now, Facebook vastly improved Mercurial
You should already be acquainted with the tool, this is an express guide/recap
Sub-commands and files are in monospace
, concepts in italic
.git
folder.git
folder marks the root of the repositorygit init
The changes that will be saved next
git add <files>
<files>
into the stagegit reset <files>
<files>
from the stage.gitignore
--force
.gitattributes
* text=auto eol=lf
*.[cC][mM][dD] text eol=crlf
*.[bB][aA][tT] text eol=crlf
*.[pP][sS]1 text eol=crlf
git commit
git tag -a
master
main
git checkout
-b
)git branch
-d
) of branchesgit merge
--no-ff
)git remote
git clone
init
HEAD
is)git fetch <remote>
<remote>
git pull <remote> <branch>
git fetch && git merge FETCH_HEAD
git push <remote> <branch>
Discussed in Software Process Engineering
with great power comes great responsibility
and also
power is nothing without control
Elements to consider:
Single branch, shared truth repository, frequent merges
Multiple branches, shared truth repository
sprouts from develop
develop
sprouts from develop
master
and develop
sprouts from master
master
and develop
These copies are called forks
Documentation of a project is part of the project
Two possibilities:
git checkout --orphan <branchname>
docs/
folder in a root of some branch
master
or any other branchOnce done, enable GitHub pages on the repository settings:
https://<username>.github.io/<reponame>
https://<username>.github.io/
<username>.github.io
https://<organization>.github.io/
<organization>.github.io
The process of creating tested deployable software artifacts
from source code
May include, depending on the system specifics:
Custom: select some phases that the product needs and perform them.
Standard: run a sequence of pre-defined actions/phases.
Automation of the build lifecycle
Different lifecycle types generate different build automation styles
Imperative: write a script that tells the system what to do to get from code to artifacts
Declarative: adhere to some convention, customizing some settings
Create a declarative infrastructure upon an imperative basis, and allow easy access to the underlying machinery
DSLs are helpful in this context: they can “hide” imperativity without ruling it out
Still, many challenges remain open:
java.*
, or scala.*
, or kotlin.*
)Example requirements:
package it.unibo.sampleapp;
import com.omertron.omdbapi.OmdbApi;
import com.omertron.omdbapi.tools.OmdbBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import static org.jooq.lambda.Unchecked.function;
/**
* Monolitic application that fetches movie rates.
*/
public final class RateAMovie {
private static final String DEFAULT_MOVIE = "Breaking Bad";
private static final String OMDB_API_KEY = System.getenv("OMDB_API_KEY");
private static final Logger LOGGER = LoggerFactory.getLogger(RateAMovie.class);
private RateAMovie() { }
/**
* Launches the application. Expects {@code OMDB_API_KEY} to be a valid environment variable.
*
* @param args a string with the movie/series name.
*/
public static void main(final String[] args) {
if (OMDB_API_KEY == null || OMDB_API_KEY.isBlank()) {
LOGGER.error("Invalid OMDB API Key '{}', set a valid API Key as env variable OMDB_API_KEY", OMDB_API_KEY);
System.exit(1);
}
final String[] titles = args.length == 0 ? new String[] { DEFAULT_MOVIE } : args;
final OmdbApi omdb = new OmdbApi(OMDB_API_KEY);
Arrays.stream(titles)
.map(function(omdb::search)) // Exceptions become RuntimeExceptions
.flatMap(searchResult -> searchResult.getResults().stream()) // get all results and flatten
.map(function(movie -> // then get more details
new OmdbBuilder().setApiKey(OMDB_API_KEY).setPlotFull().setImdbId(movie.getImdbID()).build())
)
.map(function(omdb::getInfo))
.forEach(movie ->
LOGGER.info(
"""
{}
Directed by {}; {}; {}
Written by {}
Cast: {}
{} {}
IMDb {} (over {} votes), Rotten Tomatoes {}. {}
{}
IMDb handle: {}
""",
movie.getTitle(),
movie.getDirector(),
String.join(", ", movie.getCountries()),
movie.getYear(),
movie.getWriter(),
movie.getActors(),
movie.getRuntime(),
movie.getGenre(),
movie.getImdbRating(),
movie.getImdbVotes(),
movie.getTomatoRating(),
movie.getAwards(),
movie.getPlot(),
movie.getImdbID()
)
);
}
}
+--- com.omertron:API-OMDB:1.5
| +--- commons-codec:commons-codec:1.10
| +--- org.apache.commons:commons-lang3:3.4
| +--- com.fasterxml.jackson.core:jackson-core:2.8.7
| +--- com.fasterxml.jackson.core:jackson-annotations:2.8.7
| +--- com.fasterxml.jackson.core:jackson-databind:2.8.7
| | +--- com.fasterxml.jackson.core:jackson-annotations:2.8.0 -> 2.8.7
| | \--- com.fasterxml.jackson.core:jackson-core:2.8.7
| +--- org.slf4j:slf4j-api:1.7.24 -> 1.7.36
| \--- org.yamj:api-common:2.1
| +--- org.apache.httpcomponents:httpclient:4.5.3
| | +--- org.apache.httpcomponents:httpcore:4.4.6
| | +--- commons-logging:commons-logging:1.2
| | \--- commons-codec:commons-codec:1.9 -> 1.10
| \--- org.slf4j:slf4j-api:1.7.24 -> 1.7.36
+--- org.jooq:jool:0.9.14
+--- org.slf4j:slf4j-api:1.7.36
\--- ch.qos.logback:logback-classic:1.2.11
+--- ch.qos.logback:logback-core:1.2.11
\--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
In large projects, transitive dependencies dominate
Source import
Duplication, more library code than business code, updates almost impossible, inconsistencies, unmaintainable
Binary import
Hard to update, toxic for the VCS
Desiderata
A paradigmatic example of a hybrid automator:
plugins
and buildscript
blocks)+-- src # All sources
+-- main # One folder per "source set", all code in a source set shares dependencies
| +-- java # One folder per language
| +-- kotlin
| +-- resources # Resources go here
| \-- scala
\-- test # Test sources are separated, different dependencies
+-- java # Same structure as main
+-- kotlin
+-- resources # Resources go here
\-- scala
src/main/java/HelloWorld.java
public class HelloWorld {
public static void main(String... args) {
System.out.println("Hello, world!");
}
}
build.gradle.kts
plugins { java }
Yes, it’s a one-liner
gradle
command accepts the names of tasks to execute as parametersjava
plugin introduces several tasks:
compile<source set><language name>
(e.g., compileTestJava
): compiles a specific source setcompile<language name>
: compiles all source sets of a languagebuild
: runs all compilation taskstasks
: displays available tasks
tasks --all
to show also uncategorized tasksrepositories
Well known repositories for Java and related technologies (including Scala):
mavenCentral()
– de-facto standardgoogle()
jcenter()
maven { url("https://jitpack.io") }
dependencies
api
(libraries only): abstractions are exposed publicly and clients are expected to use themimplementation
: used internally, clients should not caretestImplementation
: used to compile and run teststest
)compileOnly
: only available when compiling (typically used for annotations)test
)runtimeOnly
: only required at runtime (typically used when components are loaded reflectively)<group>:<artifact>:<version>
+
is a special marker for “latest”no guarantee that automation written with some tool at version X
, will work at version Y
!
A global dependency on the build tool is hard to capture
Often, it becomes a prerequisite expressed in natural language
Critical issues when different pieces of the same system depend on different build tool versions
Gradle proposes a (partial) solution with the so-called Gradle wrapper
wrapper
gradle wrapper --gradle-version=<VERSION>
gradlew
gradlew.bat
The Gradle wrapper is the correct way to use gradle, and we’ll be using it from now on.
src/main/java/it/unibo/sampleapp/RateAMovie.java
package it.unibo.sampleapp;
import com.omertron.omdbapi.OmdbApi;
import com.omertron.omdbapi.tools.OmdbBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import static org.jooq.lambda.Unchecked.function;
/**
* Monolitic application that fetches movie rates.
*/
public final class RateAMovie {
private static final String DEFAULT_MOVIE = "Breaking Bad";
private static final String OMDB_API_KEY = System.getenv("OMDB_API_KEY");
private static final Logger LOGGER = LoggerFactory.getLogger(RateAMovie.class);
private RateAMovie() { }
/**
* Launches the application. Expects {@code OMDB_API_KEY} to be a valid environment variable.
*
* @param args a string with the movie/series name.
*/
public static void main(final String[] args) {
if (OMDB_API_KEY == null || OMDB_API_KEY.isBlank()) {
LOGGER.error("Invalid OMDB API Key '{}', set a valid API Key as env variable OMDB_API_KEY", OMDB_API_KEY);
System.exit(1);
}
final String[] titles = args.length == 0 ? new String[] { DEFAULT_MOVIE } : args;
final OmdbApi omdb = new OmdbApi(OMDB_API_KEY);
Arrays.stream(titles)
.map(function(omdb::search)) // Exceptions become RuntimeExceptions
.flatMap(searchResult -> searchResult.getResults().stream()) // get all results and flatten
.map(function(movie -> // then get more details
new OmdbBuilder().setApiKey(OMDB_API_KEY).setPlotFull().setImdbId(movie.getImdbID()).build())
)
.map(function(omdb::getInfo))
.forEach(movie ->
LOGGER.info(
"""
{}
Directed by {}; {}; {}
Written by {}
Cast: {}
{} {}
IMDb {} (over {} votes), Rotten Tomatoes {}. {}
{}
IMDb handle: {}
""",
movie.getTitle(),
movie.getDirector(),
String.join(", ", movie.getCountries()),
movie.getYear(),
movie.getWriter(),
movie.getActors(),
movie.getRuntime(),
movie.getGenre(),
movie.getImdbRating(),
movie.getImdbVotes(),
movie.getTomatoRating(),
movie.getAwards(),
movie.getPlot(),
movie.getImdbID()
)
);
}
}
settings.gradle.kts
rootProject.name = "sample-gradle-project"
(one-liner)
build.gradle.kts
plugins {
java
application
}
repositories { // Where to search for dependencies
mavenCentral()
}
dependencies {
// Suppressions for SpotBugs
compileOnly("com.github.spotbugs:spotbugs-annotations:4.9.3")
// Maven dependencies are composed by a group name, a name and a version, separated by colons
implementation("com.omertron:API-OMDB:1.5")
implementation("org.jooq:jool:0.9.15")
implementation("org.slf4j:slf4j-api:2.0.17")
runtimeOnly("ch.qos.logback:logback-classic:1.5.18")
}
application {
mainClass.set("it.unibo.sampleapp.RateAMovie")
}
application
plugin introduces a run
task:
build
runtimeClasspath
to the -cp
option of java
./gradlew
runNote: exectution requires a valid OMDB API Key stored as an environment variable OMDB_API_KEY
src/main/groovy/HelloGroovy.groovy
println "Hello, Groovy"
src/main/java/HelloWorld.java
public class HelloWorld {
public static void main(String... args) {
System.out.println("Hello, world!");
}
}
src/main/kotlin/HelloKt.kt
fun main() {
println("Hello, world!")
}
src/main/scala/HelloScala.scala
@main def main(): Unit = println("Hello Scala")
build.gradle.kts
plugins {
java
scala
groovy
kotlin("jvm") version "2.1.20"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.codehaus.groovy:groovy:3.0.24")
implementation(kotlin("stdlib"))
implementation("org.scala-lang:scala3-library_3:3.6.4")
}
“It works” is not good enough
(besides, the very notion of “it works” is rather debatable)
Automated checkers are also called linters, often provide an auto-formatting tool
Idiomatic and standardized code:
In Java: Checkstyle, PMD
In Kotlin: IDEA Inspection, Ktlint, Detekt
In Scala: Scalafmt, Scalastyle, Wartremover,
Identification and reporting of patterns known to be problematic
In Kotlin: Detekt IDEA Inspection
In Scala: Scalafix, Wartremover
Code replicated rather than reused
General advice: never copy/paste your code
Multi-language tool: Copy/Paste Detector (CPD) (part of PMD)
Automated software verification
Extension of testing can be evaluated via coverage.
Several frameworks, recommended ones:
src/main/scala/it/unibo/test/Test.scala
package it.unibo.test
trait Sometrait:
def f: Int
def g: Int
object Someobject:
def f = 10
def g = 77
src/test/scala/MyTest.scala
import it.unibo.test.{Someobject, Sometrait}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.Test
class MyTest:
@Test
def emptySetShouldHaveSizeZero(): Unit =
assertEquals(0, Set.empty.size)
@Test
def thisTestIsActuallyUseless(): Unit =
val someStub = new Sometrait:
override def f: Int = 42
override def g: Int = ???
assertEquals(42, someStub.f)
assertNotEquals(Someobject.f, someStub.f)
build.gradle.kts
plugins {
java
scala
jacoco
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.scala-lang:scala3-library_3:3.6.4")
// The BOM (Bill of Materials) synchronizes all the versions of Junit coherently.
testImplementation(platform("org.junit:junit-bom:5.12.1"))
// The annotations, assertions and other elements we want to have access to when compiling our tests.
testImplementation("org.junit.jupiter:junit-jupiter")
// The engine that must be available at runtime to run the tests.
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// Enables JUnit Platform (needed for JUnit 5)
tasks.named<Test>("test") {
useJUnitPlatform()
}
Let’s switch testing framework and enable coverage
src/main/scala/it/unibo/test/Test.scala
package it.unibo.test
trait Sometrait:
def f: Int
def g: Int
object Someobject:
def f = 10
def g = 77
src/test/scala/Test.scala
import it.unibo.test.Sometrait
import org.scalatest.funsuite.AnyFunSuite
class MyTests extends AnyFunSuite:
test("An empty Set should have size 0") {
println("Test1")
assert(Set.empty.isEmpty)
}
build.gradle.kts
plugins {
java
scala
id("com.github.maiflai.scalatest") version "0.33"
}
repositories {
mavenCentral()
}
dependencies {
val scalaVersion = "3.6.4"
val (scalaMinor, _) = requireNotNull(Regex("^(\\d+)(\\.\\d+)(\\.\\d+)?$").matchEntire(scalaVersion)).destructured
// https://mvnrepository.com/artifact/org.scala-lang/scala3-library
implementation("org.scala-lang:scala3-library_3:${scalaVersion}")
testImplementation("org.scalatest:scalatest_$scalaMinor:3.2.12")
testRuntimeOnly("com.vladsch.flexmark:flexmark-all:0.64.8")
}
java
plugin (applied by the scala
plugin under the hood) also introduces:
test
: a task that runs all testscheck
: a task that runs the whole quality assurance suiteThere exist a number of recommended services that provide additional QA and reports.
Non exhaustive list:
The practice of integrating code with a main development line continuously
Verifying that the build remains intact
Traditionally, protoduction is jargon for a prototype that ends up in production
Software that promotes CI practices should:
Plenty of integrators on the market
Circle CI, Travis CI, Werker, done.io, Codefresh, Codeship, Bitbucket Pipelines, GitHub Actions, GitLab CI/CD Pipelines, JetBrains TeamCity…
Naming and organization is variable across platforms, but in general:
In essence, designing a CI system is designing a software construction, verification, and delivery pipeline with the abstractions provided by the selected provider.
Configuration can grow complex, and is usually stored in a YAML file
(but there are exceptions, JetBrains TeamCity uses a Kotlin DSL).
step
action
job
workflow
Workflows are configured in the .github/workflows/
folder of the repository
Workflows react to events
on
keyon:
push: # Trigger the workflow on push
branches:
- main # but only for the main branch
pull_request: # Also trigger for pull requests
branches:
- main # but only for the main branch
page_build: # Also trigger on page_build
release: # Also trigger on release...
types:
- created # ...created
schedule:
- cron: '*/30 5,17 * * *' # Cron syntax (see crontab.guru)
GitHub actions job run on a fresh virtual machine
Rather than convention over configuration, GHA use actions as a form of configuration reuse
name: Clone Repository
on:
push: # On every push on any branch
jobs:
Build: # Job name
runs-on: ubuntu-latest # Operating system selection
env: # Environment can be controlled at the job or step level
TERM: dumb # Sets the environment variable TERM to "dumb"
steps:
- name: Checkout # custom name Checkout the repository
uses: actions/checkout@v2 # Action implemented in this repository, tag "2"
- uses: joschi/setup-jdk@v2.3.0 # Action implemented in repository "joschi/setup-jdk" tag "2.3.0"
with: # Actions parameters (action name omitted)
java-version: 15
- name: Build
run: ./gradlew check
Once the reference environment in CI completed the software construction and testing, it should sign and then deliver the result.
In this repository, delivery is enabled towards all the aforementioned destinations
Your error will remain in the repositories forever and you will never be able to fix it, you will need to create a new release with a different version number.
In March 2016, Azer Koçulu unpublished more than 250 of his modules from NPM, which is a popular package manager used by Javascript projects to install dependencies, because he was asked to rename the module
Kik
, whose name is the same of an instant messaging app that wanted to publish a homonym module. Unfortunately, one of those dependencies wasleft-pad
, a 11-line-long Javascript module, used by thousands of projects. This brought breackages throughout many Javascript products (most notably Node and Babel). NPM had to un-unpublishleft-pad
.
Without compliance to some sort of formal specification, version numbers are essentially useless for dependency management. By giving a name and clear definition to the above ideas, it becomes easy to communicate your intentions to the users of your software.
MAJOR.MINOR.PATCH[-OTHER][+BUILD]
MAJOR
— Any incompatible API change
0
is for initial development: anything may change at any time.MINOR
— Add functionality in a backwards-compatible mannerPATCH
— Backwards-compatible bug fixesOTHER
— Optionally decorates the version with additional information.BUILD
— Optionally decorates the version with build information.0.1.0
, 1.0.0
formalizes the API