
Spesso mi capita di dover costruire un pipeline per la compilazione di progetti in Java. A volte si tratta di open source, altre volte no. Recentemente ho deciso di provare a trasferire alcuni dei miei repository da Travis-CI e TeamCity su GitHub Actions, ecco cosa ne è venuto fuori.
Cosa automatizzeremo
Per iniziare abbiamo bisogno di un progetto che vogliamo automatizzare, realizziamo una piccola applicazione con Spring Boot / Java 11 / Maven. In questo articolo, la logica dell'applicazione non ci interesserà affatto, ciò che ci importa è l'infrastruttura attorno all'applicazione, quindi ci servirà un semplice controller REST API.
Puoi vedere il sorgente qui: tutti i passaggi per la costruzione del pipeline sono riflessi nelle pull request di questo progetto.
JIRA e pianificazione
Vale la pena dire che di solito utilizziamo JIRA come tracker di attività, quindi creiamo una bacheca separata per questo progetto e iniziamo a buttare giù i primi task:

Torneremo più tardi su cosa possono offrire di interessante JIRA e GitHub insieme.
Automatizziamo la costruzione del progetto
Il nostro progetto di test viene costruito tramite Maven, quindi la sua compilazione è abbastanza semplice, tutto ciò di cui abbiamo bisogno è mvn clean package.
Per fare ciò utilizzando GitHub Actions, dovremo creare un file nella repository con la descrizione del nostro workflow, possiamo farlo con un normale file yml, non posso dire che mi piaccia "programmare in yml", ma cosa possiamo fare — creiamo nella directory .github/workflow/ il file build.yml in cui descriveremo le azioni per la costruzione del ramo master:
name: Build
on:
pull_request:
branches:
- '*'
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Maven Package
run: mvn -B clean package -DskipTests
on — è la descrizione dell'evento che attiverà il nostro script.
on: pull_request / push — indica che questo workflow deve essere eseguito ad ogni push sul master e per la creazione delle pull request.
Segue la descrizione dei compiti (jobs) e i passaggi di esecuzione (steps) per ogni compito.
runs-on — qui possiamo scegliere il sistema operativo di destinazione, sorprendentemente si può anche scegliere Mac OS, ma su repository privati è un'opzione piuttosto costosa (rispetto a linux).
uses ci consente di riutilizzare altre azioni, ad esempio con l'azione actions/setup-java installiamo l'ambiente per Java 11.
Utilizzando con possiamo specificare i parametri con cui lanciamo l'azione, in sostanza sono argomenti che verranno passati all'azione.
Rimane solo da avviare la build del progetto con maven: run: mvn -B clean package flag -B indica che abbiamo bisogno di una modalità non interattiva, in modo che maven non ci chieda nulla.

Ottimo! Ora, ad ogni commit nel master, viene avviata la build del progetto.
Automatizziamo l'esecuzione dei test
La build va bene, ma in realtà un progetto può costruirsi correttamente ma non funzionare. Pertanto, il passo successivo è dedicarsi all'automazione dell'esecuzione dei test. Inoltre, è piuttosto comodo vedere i risultati dei test durante la revisione di una PR — sai esattamente che i test stanno passando e che nessuno si è dimenticato di eseguire la propria branch prima di fare il merge.
Eseguiamo i test al momento della creazione di una pull request e del merge nel master, e aggiungiamo anche la costruzione del report sul code coverage.
name: Build
on:
pull_request:
branches:
- '*'
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Maven Verify
run: mvn -B clean verify
- name: Test Coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
Per la copertura dei test utilizzo codecov insieme al plugin jacoco. Codecov ha la sua azione, ma ha bisogno di un token per lavorare con la nostra pull request:
${{ secrets.CODECOV_TOKEN }} — incontreremo ancora una volta questa struttura, 'secrets' è un meccanismo per la memorizzazione dei segreti su GitHub, dove possiamo scrivere password/token/host/url e altri dati che non dovrebbero essere esposti nel codice del repository.
Aggiungere una variabile ai segreti può essere fatto nelle impostazioni del repository su GitHub:

Puoi ottenere il token su dopo l'autenticazione tramite GitHub, per aggiungere un progetto pubblico basta seguire un link del tipo: /[repo name]. Приватный репозиторий тоже можно добавить, для этого надо дать права codecov приложению в гитхабе.

Aggiungiamo il plugin jacoco nel file POM:
org.jacoco
jacoco-maven-plugin
0.8.4
prepare-agent
report
test
report
org.apache.maven.plugins
maven-surefire-plugin
2.22.2
plain
**/*Test*.java
**/*IT*.java
Ora in ogni nostra pull request entrerà il bot di codecov e aggiungerà un grafico delle variazioni della copertura:

Aggiungiamo un analizzatore statico
Nella maggior parte dei miei progetti open source utilizzo Sonar Cloud per l'analisi statica del codice; è abbastanza semplice collegarlo a travis-ci. Quindi, è un passo logico durante la migrazione a GitHub Actions fare la stessa cosa. Il marketplace delle azioni è una cosa carina, ma questa volta un po' ha deluso, perché sono abituato a trovare l'azione necessaria e a scriverla nel workflow. E si è scoperto che Sonar non supporta il funzionamento tramite l'azione per l'analisi dei progetti su Maven o Gradle. Certo è scritto nella documentazione, ma chi la legge?!
Non si può fare tramite l'azione, quindi faremo attraverso il plugin mvn:
name: SonarCloud
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
- name: Imposta JDK
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Analizza con SonarCloud
# impostare le variabili d'ambiente:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# eseguire il plugin sonar maven:
run: mvn -B verify sonar:sonar -Dsonar.projectKey=antkorwin_github-actions -Dsonar.organization=antkorwin-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml
SONAR_TOKEN — può essere ottenuto in e deve essere inserito nei segreti. GITHUB_TOKEN — è un token incorporato che GitHub genera; con questo Sonarcloud[bot] può autenticarsi in Git per lasciarci messaggi nelle pull request.
Dsonar.projectKey — il nome del progetto in Sonar, che può essere visualizzato nelle impostazioni del progetto.
Dsonar.organization — il nome dell'organizzazione su GitHub.
Facciamo una pull request e aspettiamo che sonarcloud[bot] arrivi nei commenti:

Gestione del rilascio
Abbiamo configurato il build, eseguito i test, ora possiamo fare un rilascio. Vediamo come GitHub Actions aiuta a semplificare significativamente la gestione del rilascio.
Al lavoro ho progetti la cui base di codice risiede in bitbucket (tutto come nella storia «scrivo di giorno in bitbucket, faccio commit di notte in GitHub»). Sfortunatamente, bitbucket non ha strumenti integrati per la gestione delle release. Questo è un problema, perché per ogni release bisogna creare manualmente una pagina in confluence e inviare lì tutte le funzionalità incluse nella release, scandagliare i meandri della mente, i task in jira, i commit nei repository. Ci sono molte probabilità di commettere errori, si può dimenticare qualcosa o scrivere ciò che è già stato rilasciato l'ultima volta, a volte non è chiaro a quale pull request attribuire — è una funzionalità o una correzione di bug, o una modifica ai test, o qualcosa di infrastrutturale.
Come può aiutarci GitHub actions? C'è un ottimo action — release drafter, che consente di impostare un modello per il file delle note di rilascio, per configurare le categorie dei pull request e raggrupparle automaticamente nel file delle note di rilascio:

Esempio di modello per la configurazione del report (.github/release-drafter.yml):
name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
- title: ' Nuove Funzionalità'
labels:
- 'type:features'
# in questa categoria raccogliamo tutti i PR con l'etichetta type:features
- title: ' Correzioni di Bug'
labels:
- 'type:fix'
# analogamente per l'etichetta type:fix e così via.
- title: ' Documentazione'
labels:
- 'type:documentation'
- title: ' Configurazione'
labels:
- 'type:config'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
## Modifiche
$CHANGES
Aggiungiamo uno script per generare la bozza di rilascio (.github/workflows/release-draft.yml):
name: "Crea bozza di rilascio"
on:
push:
branches:
- master
jobs:
update_draft_release:
runs-on: ubuntu-18.04
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Tutti i pull request da questo momento verranno raccolti automaticamente nelle note di rilascio — magia!
Qui potrebbe sorgere la domanda: e se gli sviluppatori dimenticano di aggiungere le etichette nei PR? Allora non sarà chiaro in quale categoria classificarlo e dovremo di nuovo esaminare manualmente ciascun PR separatamente. Per risolvere questo problema, possiamo utilizzare un altro action — label verifier — che verifica la presenza di tag nel pull request. Se non c'è alcun tag obbligatorio, il controllo sarà fallito e vedremo un messaggio nel nostro pull request.
name: "Verifica le etichette di tipo"
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
triage:
runs-on: ubuntu-18.04
steps:
- uses: zwaldowski/match-label-action@v2
with:
allowed: 'type:fix, type:features, type:documentation, type:tests, type:config'
Ora ogni pull-request deve essere contrassegnata con uno dei tag: type:fix, type:features, type:documentation, type:tests, type:config.

Auto-annotazione dei pull request
Dato che abbiamo toccato un tema come il lavoro efficace con i pull request, vale la pena menzionare un'azione come labeler, che aggiunge etichette ai PR in base ai file che sono stati modificati. Ad esempio, possiamo etichettare come [build] qualsiasi pull request in cui ci siano modifiche nella directory .github/workflow.
Collegarlo è piuttosto semplice:
name: "Assegna temi automaticamente ai PR"
on:
- pull_request
jobs:
triage:
runs-on: ubuntu-18.04
steps:
- uses: actions/labeler@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
Avremo anche bisogno di un file che descriva la corrispondenza delle directory del progetto con i temi dei pull request:
theme:build:
- ".github/**"
- "pom.xml"
- ".travis.yml"
- ".gitignore"
- "Dockerfile"
theme:code:
- "src/main/*"
theme:tests:
- "src/test/*"
theme:documentation:
- "docs/**"
theme:TRASH:
- ".idea/**"
- "target/**"
Non sono riuscito a far funzionare l'azione che aggiunge automaticamente le etichette ai pull request e l'azione che verifica la presenza delle etichette obbligatorie; match-label non riesce a riconoscere le etichette assegnate dal bot. Sembra più semplice scrivere una propria azione che combini entrambi i passaggi. Ma anche in questo modo, è piuttosto comodo, è necessario scegliere un'etichetta dall'elenco quando si crea un pull request.
È ora di fare il deploy

Ho provato diverse opzioni di deploy tramite GitHub Actions (tramite ssh, tramite scp, e utilizzando docker-hub), e posso dire che, molto probabilmente, troverai un modo per caricare il binario sul server, non importa quanto complesso sia il tuo pipeline.
Mi è piaciuta l'idea di tenere tutta l'infrastruttura in un unico posto, quindi vediamo come fare il deploy in GitHub Packages (questo è un repository per contenuti binari, npm, jar, docker).
Script per costruire un'immagine docker e pubblicarla in GitHub Packages:
nome: Distribuisci immagine docker
su:
push:
rami:
- 'master'
lavori:
build_docker_image:
runs-on: ubuntu-18.04
passaggi:
# Costruisci JAR:
- uses: actions/checkout@v1
- name: imposta JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Pacchetto Maven
run: mvn -B clean compile package -DskipTests
# Imposta variabili d'ambiente globali:
- name: imposta env globale
id: global_env
run: |
echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*\/}"
echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com/${GITHUB_REPOSITORY}/${GITHUB_REPOSITORY#*\/}"
# Costruisci immagine Docker:
- name: Costruisci e tagga immagine
run: |
docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .
- name: Accesso Docker
run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}
# Pubblica immagine nel repository dei pacchetti di github:
- name: Pubblica immagine
env:
IMAGE_NAME: $GITHUB_REPOSITORY
run: docker push "docker.pkg.github.com/$GITHUB_REPOSITORY/${{ steps.global_env.outputs.IMAGE_NAME }}"
Per prima cosa dobbiamo costruire il file JAR della nostra applicazione, dopodiché calcoliamo il percorso del registro docker di GitHub e il nome della nostra immagine. Ci sono alcuni trucchi qui con cui non ci siamo ancora imbattuti:
- una costruzione del tipo: echo «::set-output name=NAME::VALUE» consente di impostare il valore di una variabile nel passaggio corrente, in modo che possa essere letta in tutti gli altri passaggi.
- per ottenere il valore di una variabile impostata nel passaggio precedente si può usare l'identificatore di quel passaggio: ${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}
- Nella variabile standard GITHUB_REPOSITORY è memorizzato il nome del repository e il suo proprietario («owner/repo-name»). Per estrarre da questa stringa tutto tranne il nome del repository, utilizziamo la sintassi bash: ${GITHUB_REPOSITORY#*\/}
Dopo dobbiamo costruire l'immagine docker:
docker build -t "docker.pkg.github.com/antkorwin/github-actions/github-actions:latest"
Effettuare il login nel registro:
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}
E pubblicare l'immagine nel GitHub Packages Repository:
docker push "docker.pkg.github.com/antkorwin/github-actions/github-actions"
Per specificare la versione dell'immagine, utilizziamo le prime cifre dell'hash SHA del commit — GITHUB_SHA presenta anche alcune sfide, se si eseguono queste costruzioni non solo durante il merge nel master, ma anche in risposta a eventi di creazione di pull request, l'SHA potrebbe non corrispondere all'hash che vediamo nella storia git, perché l'azione actions/checkout genera il proprio hash unico per evitare blocchi reciproci delle azioni nella PR.

Se tutto è andato a buon fine, aprendo la sezione pacchetti (https://github.com/antkorwin/github-actions/packages) nel repository, vedrai un nuovo immagine docker:

Qui puoi anche vedere l'elenco delle versioni dell'immagine Docker.
Resta solo da configurare il nostro server per lavorare con questo registry e riavviare il servizio. Di come farlo tramite systemd, ne parlerò un'altra volta.
Monitoraggio
Vediamo una variante semplice di come fare il controllo di stato della nostra applicazione utilizzando GitHub Actions. Nella nostra applicazione di avvio c'è un actuator, quindi non c'è nemmeno bisogno di scrivere un'API per verificarne lo stato, per i pigri è già tutto pronto. Basta solo richiamare l'host: SERVER-URL:PORT/actuator/health
$ curl -v 127.0.0.1:8080/actuator/health
> GET /actuator/health HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.1
> Accept: */*
< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Thu, 04 Jun 2020 12:33:37 GMT
{"status":"UP"}
Tutto ciò di cui abbiamo bisogno è scrivere un task di verifica del server cron, e se per caso non ci risponde, invieremo una notifica su Telegram.
Iniziamo a capire come avviare il workflow tramite cron:
on:
schedule:
- cron: '*\/5 * * * *'
È tutto molto semplice, non ci riesco a credere che su GitHub si possano creare eventi che non rientrano affatto nei webhook. I dettagli sono nella documentazione:
Controlliamo lo stato del server manualmente tramite curl:
jobs:
ping:
runs-on: ubuntu-18.04
steps:
- name: curl actuator
id: ping
run: |
echo "::set-output name=status::$(curl ${{secrets.SERVER_HOST}}/api/actuator/health)"
- name: health check
run: |
if [[ ${{ steps.ping.outputs.status }} != *"UP"* ]]; then
echo "il controllo di stato è fallito"
exit 1
fi
echo "Va tutto bene"
Per prima cosa salviamo in una variabile ciò che ha risposto il server alla richiesta, nel passo successivo verifichiamo che lo stato sia UP e, se non lo è, usciamo con un errore. Se è necessario "fallire" manualmente l'azione, allora exit 1 — un'arma adatta.
- name: send alert in telegram
if: ${{ failure() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
Il controllo di stato del:
${{secrets.SERVER_HOST}}/api/actuator/health
è fallito con il risultato:
${{ steps.ping.outputs.status }}
Invieremo su Telegram solo se l'azione è fallita nel passo precedente. Per inviare il messaggio utilizziamo appleboy/telegram-action; per sapere come ottenere il token del bot e l'ID della chat, puoi consultare la documentazione:

Non dimenticare di inserire nei segreti di GitHub: l'URL per il server e i token per il bot Telegram.
Traccia bonus — JIRA per i pigri
Ho promesso che saremmo tornati su JIRA, e lo abbiamo fatto. Centinaia di volte ho osservato durante i stand-up la situazione in cui gli sviluppatori hanno creato una funzionalità, hanno unito il ramo, ma si sono dimenticati di trascinare il compito in JIRA. Certo, se tutto avvenisse in un unico posto, sarebbe più semplice, ma di fatto scriviamo codice in IDE, uniamo i rami in Bitbucket o GitHub, e i compiti poi li trasferiamo in Jira, per questo dobbiamo aprire nuove finestre, a volte dover effettuare di nuovo il login, ecc. Quando ricordi perfettamente cosa devi fare dopo, non ha senso aprire la board ancora una volta. Alla fine, al mattino, durante lo stand-up, bisogna sprecare tempo per aggiornare la bacheca delle attività.
GitHub ci aiuterà anche in questa attività noiosa, per iniziare possiamo trascinare automaticamente i compiti nella colonna code_review quando pubblichiamo una pull request. Tutto ciò che serve è seguire la convenzione nella denominazione dei rami:
[nome progetto]-[numero compito]-titolo
ad esempio, se la chiave del progetto «GitHub Actions» sarà GA, allora GA-8-jira-bot può essere un ramo per l'implementazione del compito GA-8.
L'integrazione con JIRA funziona tramite azioni di Atlassian, non sono perfette, devo dire che alcune di esse non hanno funzionato affatto per me. Ma discuteremo solo quelle che funzionano sicuramente e vengono utilizzate attivamente.
Per prima cosa, è necessario effettuare l'autenticazione in JIRA utilizzando l'azione: atlassian/gajira-login
jobs:
build:
runs-on: ubuntu-latest
name: Jira Workflow
steps:
- name: Login
uses: atlassian/gajira-login@master
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
Per questo è necessario ottenere un token in JIRA, come fare è descritto qui:
Estraiamo l'identificatore del compito dal nome del ramo:
- name: Find Issue
id: find_issue
shell: bash
run: |
echo "::set-output name=ISSUE_ID::$(echo ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}')"
echo nome ramo: $GITHUB_HEAD_REF
echo problema estratto: ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}'
- name: Check Issue
shell: bash
run: |
if [[ "${{steps.find_issue.outputs.ISSUE_ID}}" == "" ]]; then
echo "Per favore dai un nome al tuo ramo secondo il compito JIRA: [project_key]-[task_number]-branch_name"
exit 1
fi
echo trovato con successo il compito JIRA: ${{steps.find_issue.outputs.ISSUE_ID}}
Se cerchi nel marketplace di GitHub, puoi trovare un'azione per questo compito, ma ho dovuto scriverne una simile tramite grep basandomi sul nome del ramo, perché questa azione di Atlassian non ha assolutamente voluto funzionare nel mio progetto, e capire cosa non andasse richiederebbe più tempo rispetto a fare la stessa cosa manualmente.
Rimane solo da spostare il compito nella colonna «Code review» durante la creazione della pull request:
- name: Transizione del problema
if: ${{ success() }}
uses: atlassian/gajira-transition@master
with:
issue: ${{ steps.find_issue.outputs.ISSUE_ID }}
transition: "Code review"
A questo scopo, esiste un'azione speciale su GitHub; tutto ciò di cui ha bisogno è l'identificativo del problema ottenuto nel passaggio precedente e l'autenticazione in JIRA, che abbiamo fatto in precedenza.

Allo stesso modo, è possibile trascinare i problemi durante il merge nel master e in altri eventi del workflow di GitHub. In generale, tutto dipende dalla tua fantasia e dalla volontà di automatizzare i processi ripetitivi.
Conclusioni
Se guardiamo al diagramma classico di DEVOPS, abbiamo coperto tutte le fasi, tranne forse l'operazione; penso che, se ci si impegna, si possa trovare qualche azione nel marketplace per l'integrazione con un sistema di help-desk, quindi possiamo considerare che il pipeline sia solido e che, sulla base del suo utilizzo, si possano trarre conclusioni.

Pro:
- Il Marketplace con azioni pronte all'uso per tutte le occasioni è davvero fantastico. Nella maggior parte di essi è possibile anche visualizzare il codice sorgente per capire come risolvere problemi simili o inviare una richiesta di funzionalità all'autore direttamente nel repository di GitHub.
- La scelta della piattaforma di destinazione per la compilazione: Linux, macOS, Windows è una funzione piuttosto interessante.
- GitHub Packages è una grande cosa; tenere tutta l'infrastruttura in un unico posto è comodo, non devi navigare tra diverse finestre, tutto è a portata di un click e ben integrato con GitHub Actions. Il supporto per il Docker registry nella versione gratuita è un altro buon vantaggio.
- GitHub nasconde i segreti nei registri di compilazione, quindi utilizzarlo per memorizzare password e token non è così spaventoso. Durante tutti i miei esperimenti, non sono mai riuscito a vedere un segreto in chiaro nella console.
- È gratuito per progetti Open Source
Contro:
- YML, non mi piace. Lavorando con questo tipo di flusso, il mio messaggio di commit più comune è «fix yml format», a volte dimentico di mettere un tab oppure scrivo nella riga sbagliata. Insomma, stare davanti al computer con un goniometro e un righello non è il modo più piacevole di lavorare.
- DEBUG, fare debugging del flusso con commit, avviando il rebuild e visualizzando in console non è sempre conveniente, ma è più una questione di «siete viziati», abituati a lavorare con IDE comode, dove puoi fare il debug di qualsiasi cosa.
- Puoi scrivere la tua azione su qualsiasi cosa se la confezioni in Docker, ma nativamente viene supportato solo JavaScript; ovviamente è una questione di gusti, ma preferirei qualcos'altro al posto di js.
Ricordo che il repository con tutti gli script si trova qui:
La prossima settimana parlerò a alla conferenza Heisenbug 2020 Piter. Non solo parlerò di come evitare errori nella preparazione dei dati di test, ma condividerò anche i miei segreti su come lavorare con set di dati in applicazioni Java!
Fonte: habr.com
