Hay mucha teoría sobre pipelines de CI/CD, en artículos y libros pero no me ha resultado sencillo encontrar un ejemplo de implementación con todas esas buenas prácticas lo suficientemente genérico para que se adapte en gran medida a cualquier organización. En este artículo muestro un ejemplo de pipeline con workflows reutilizables usando Github Actions, Task, rease-action y git-cliff junto con algún script de bash. También comento algunos aspecto de contexto por el que surge la necesidad y los pasos hasta llegar a esta nueva implementación.
A lo largo del tiempo las empresas generan inevitablemente software heredado (o cantidades ingentes de software heredado) u obsoleto por falta de tiempo para su mantenimiento, conocimiento o personas que ya no están en la empresa.
Para algunas empresas fundamentalmente tecnológicas es un problema ya que su valor y competitividad se asienta en la tecnología, a día de hoy la totalidad de las empresas son en gran medida empresas de tecnología y dependen de ella, y o son buenas en desarrollar software o seguramente no prosperen.
No poseer una buena tecnología con la que ofrecer sus servicios puede significar no generar beneficios y no ser competitiva que mantenido en el tiempo de una forma u otra el fracaso como compañía seguramente después de momentos dolorosos con varios procesos de despidos. También puede significar la pérdida de personas y su talento o ser incapaz de atraerlo, profesionalmente es más difícil que alguien esté interesado en una empresa si no la considera un ámbito atractivo profesionalmente en la que pueda aprender y crecer, para muchas personas trabajar con herramientas actuales es un requisito. Trabajar con código heredado puede tener su atractivo siempre y cuando haya oportunidades y voluntad de modernizarlo.
Y dicho esto una de las oportunidades en las que he podido cambiar en un contexto de mucho código heredado ha sido el pipeline de CI/CD, al menos para los proyectos modernos o en los que los cambios son posibles. Y después de leer el artículo si quieres comentar, ¿como es el pipeline de CI/CD que usas en el trabajo? ¿que herramientas usas? ¿está completamente automatizado o hay pasos manuales? ¿hacéis teses funcionales una vez desplegado? ¿si haces algo diferente en tu empresa, que podría mejorar en este? Deja un comentario estaré encantado de leerlo y de aprender.
El pipeline de integración continua y despliegue continuo
A día de hoy un pipeline de integración continua y despliegue es indispensable, su función es integrar los cambios de los desarrolladores y con pruebas unitarias validar que los cambios de código no introducen errores. Hay mucha teoría y libros explicando las importantes ventajas que proporciona esto. Además de descubrir errores, un pipeline de CI permite hacerlo rápido y de forma automatizada, lo que mejora la eficiencia y rapidez en el desarrollo de software. Ciclos rápidos de desarrollo permiten experimentar y obtener feedback antes.
El siguiente paso en la automatización es el despliegue continuo o continous deploy con el que el despliegue en el entorno de producción queda también automatizado. Automatizar el despliegue permite reducir el tiempo en introducir cambios, de forma más eficiente con menos esfuerzo y más fiable con menos errores. Quizá requiera una aprobación o despliegue en el momento deseado pero mayormente el despliegue está automatizado.
El último paso es la entrega continua o continous delivery con la que los cambios se despliegan en producción si todas las pruebas automatizadas validan el software correctamente, el software se despliega en producción totalmente automatizada sin aprobaciones. Esto requiere de pruebas automatizadas adicionales como teses de aceptación, funcionales, de seguridad o rendimiento.
Con todas las ventajas de la integración continua y el despliegue continuo el desarrollo de software actual se hace empleando estas técnicas de ingeniería de software.
Contexto
Uno de los puntos en el que había mucho legacy en la empresa en la que trabajo era el CI/CD, más que él es los varios que había (Jenkins y Concourse), bueno ahora hay uno más pero este más moderno que dedicando tiempo ya ha demostrado ser capaz de reemplazar Concourse con lo que es cuestión de tiempo que desaparezca. Y estoy vislumbrando alguna opción viable para reemplazar Jenkins que es una infraestructura más antigua.
El inicial era un Jenkins y luego algunos proyecto más modernizados pasaron a Concourse con ambos pipelines de integración continua funcionando. Los despliegues con Jenkins requería de varios pasos manuales con mucho margen de mejora en la automatización, toma de 3 a 4 horas hacer un despliegue y esto ya es siendo bastante eficiente en el proceso. Esos Jenkins no eran un servicio administrado que había que mantener y dedicar tiempo a que sus instancias de computación funcionasen correctamente, además los pipelines no están bajo el control de los servicios que los hace poco flexibles, impone limitaciones en los nuevos servicio o requiere seguir las convenciones. Su coste era fijo independientemente de si se usaba o no.
Con Concourse las cosas son un poco mejores pero no usa algunas buenas prácticas, los pipelines de despliegue están separados de los proyectos y cada grupo de aplicaciones tiene su propio repositorio de pipeline de CI/CD con lo que en realidad había varios pipelines aún usando el mismo Concourse, que se ha convertido en código heredado difícil de mantener y aplicar cambios. Es un servicio administrado pero las alternativas son mejores ya no solo porque otras están mejor documentadas.
Primera solución con Github Actions
Hace un tiempo Github añadió como complemento natural denominado Github Actions que en esencia es una herramienta de CI/CD más moderna. Uno de sus atractivos es que es un servicio administrado con lo que no requiere dedicar tiempo ni personas a administrarlo y está integrado con los repositorios de git en Github, por otro lado cualquiera con cuenta en Github puede usarlo con lo que muchos profesionales ya tienen conocimiento avanzado de como usarlo, tiene un coste superado un límite de uso pero es un coste por uso y no fijo independientemente de si se usa o no.
En un primer momento el uso que le dábamos era para pasar los teses unitarios con integración continua para cada push a un pull request y en el merge a la rama main. Luego para hacer el despliegue de algunos servicios en Google Kubernetes Engine, más tarde en Google App Engine. En un primer momento solo para los servicios de backend y más tarde incorporando los servicios de frontend. Con lo que ahora no solo hay un único pipeline y este sirve tanto para los servicios de backend como de frontend, un ahorro considerable en tiempo. Luego también para enviar notificaciones a Slack en determinados canales para advertir de un nuevo despliegue y diversa información, integración con SonarCloud con métricas de calidad de código y Jira para extraer información de la petición asociada al pull request y generar releases de Github y archivos con lista de cambios.
Github Actions tiene un marketplace de acciones en el que hay integraciones para la totalidad de herramientas populares y muchas de las menos conocidas, con lo que usar algo es sencillo, rápido y da mucha flexibilidad en caso de surgir una nueva necesidad.
Nuevo contexto
Usar Github Actions para la integración continua con teses unitarios y ser un servicio administrado ya era una mejora pero con una base de cientos de repositorios no es escalable ir replicando en cada repositorio el pipeline de CI/CD. De forma que ahora surge la necesidad de construir un pipeline reusable y suficientemente genérico para que cubra la necesidad como pipeline de los servicios tanto de backend como de frontend.
Esto significa separar los repositorios del código de los pipeline nuevamente pero es una contrapartida opcional y preferible que copiar y pegar en la multitud de repositorios de código. Quizá con un monorepo sería otra la herramienta a utilizar pero en el contexto actual de multitud de repositorios es la opción viable, por tiempo, garantía de éxito y esfuerzo.
Utilizando Google Cloud el nuevo pipeline ha de soportar varios artefactos de construcción como Dockerfiles, librerías java y desplegar en Google Cloud Platform, Google Kubernetes Engine (GKE), Google App Engine (GAE), Google Cloud Functions o publicar las librerías en repositorios de Maven o NPM en Google Artifact Registry, de tal forma que el pipeline lo permita y suficientemente flexible para que el código no sea demasiado complicado además se ser escalable y permitir en el futuro añadir nuevos lenguajes, artefactos y entornos de ejecución si fuera necesario.
Nueva solución con GitHub Actions
Así que toca buscar herramientas e implementar una solución usando Github Actions de tal forma que el nuevo pipeline sea reutilizable, suficientemente flexible para que cubra las diferentes necesidades y que sea suficientemente sencillo para que no sea un problema de mantenimiento.
Además de tener que construir diferentes artefactos, de poder desplegar en diferentes runtimes ha de soportar repositorios en diferentes lenguajes como Javascript, Typescript, Node y Java en diferentes versiones cada uno.
Para soportar esta diversidad de lenguajes y versiones el pipeline ha de independizarse y no acoplarse a cada lenguaje actual y los hipotéticos futuros, por ejemplo el comando de las tareas para los teses unitarios y análisis estático de código son diferentes para un proyecto de Node y otro de Java, GNU Make es una herramienta que proporciona esa abstracción pero finalmente opto por Task que es una equivalente que se define más simple y fácil de usar.
Otra de las necesidades es de realizar versionado, bump commits que preparan la siguiente release y crear tags en el repositorio de git con cada nueva versión, para los proyectos Java estábamos usando el plugin de Gradle Axion pero ahora necesitando soportar varios lenguajes para resolver este problema opto por desarrollar un script de bash a medida para hacer la release ya que otras opciones no me convencieron puede porque no supe utilizar bien semantic-release.
También me ha sido necesario buscar algo de información de buenas prácticas en un pipeline en artículos y libros. Que sea sencillo y que sea rápido por ejemplo en el paso de integración continua, en los siguientes artículos y libros hay más información. Para las fases de este pipeline me he basado en Hashicorp Waypoint que define tres build, deploy y release, separando el deploy del release con el cambio de tráfico a la nueva versión y ahí entrarían diferentes estrategias de release con canary o blue-green además de como realizar las actualizaciones de las instancias o el rollout.
Enlaces.
Libros.
También he necesitado leer algunas secciones de documentación de Github Actions para aprender, por ejemplo para ver como configurar las reglas de protección de las ramas y diferentes opciones de merge.
Implementación de pipeline reutilizable con Github Actions
El pipeline como el anterior consta de tres fases principales la de build, release y deploy. La build realiza la integración continua con los teses unitarios y el análisis estático de código en un proyecto Java con Gradle, JUnit, PMD, Checkstyle, Spotbugs y SonarCloud. La fase de release genera el artefacto a desplegar y genera la release en caso de un merge a la rama principal o main. Y la fase de deploy despliega en artefacto en el runtime que corresponde y envía la notificación del despliegue a Slack que en el caso a producción sirve para propósitos de auditoría.
Como es una buena práctica que la fase de integración continua sea rápida para obtener feedback lo más rápido posible cada una de esas comprobaciones se lanza en paralelo, esto tiene más coste pero el beneficio de la inmediatez es preferible (y estar esperando también tiene un coste). En los proyectos de Node las tareas de teses unitarios y análisis estático de código serán otros. Como he mencionado para esta abstracción he utilizado Task combinado con el concepto de matrix de Github Actions para lanzarlos en paralelo y permitir configurar el caller workflow cuales son las tareas de check.
Una vez la fase de check es correcta se lanza la fase de construcción del artefacto y de generación de la release con varias tareas específicas para cada artefacto que se activan con condiciones if en función de cómo ha de construirse el artefacto. Ya sea con un Dockerfile o una librería Java, o más que se añadan posteriormente con Buildpacks por ejemplo.
Construido el artefacto hay que generar la release y publicarlo en el repositorio de artefactos, para una imagen de contenedor en un repositorio de Docker y para una librería en un repositorio de Maven o repositorio de NPM. Para etiquetar el repositorio de Git he utilizado un script de bash y para generar la release en Github release-action.
El workflow de la fase de build
Hay varios archivos de configuración importantes, el de las tareas de Task que proporcionará el repositorio que haga uso de los workflows y el del propio workflow de build.
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
|
version: '3'
dotenv: ['.env']
tasks:
build:
cmds:
- ../gradlew ${GRADLE_OPTIONS} build
schedule:
deps:
- task: schedule:jacoco
run:
cmds:
- ../gradlew ${GRADLE_OPTIONS} run
setup:install:
cmds:
- echo "setup:install"
build:check:
deps:
- task: build:test
- task: build:pmd
- task: build:checkstyle
- task: build:spotbugs
build:test:
cmds:
- ../gradlew ${GRADLE_OPTIONS} test
build:pmd:
cmds:
- ../gradlew ${GRADLE_OPTIONS} pmdMain pmdTest
build:checkstyle:
cmds:
- ../gradlew ${GRADLE_OPTIONS} checkstyleMain checkstyleTest
build:spotbugs:
cmds:
- ../gradlew ${GRADLE_OPTIONS} spotBugMain spotBugTest
build:sonar:
cmds:
- ../gradlew ${GRADLE_OPTIONS} -Dorg.gradle.project.sonarToken="${SONAR_TOKEN}" build jacocoTestReport sonar
build:version:
cmds:
- ../gradlew -q version
build:assemble:
cmds:
- ../gradlew ${GRADLE_OPTIONS} clean assemble
build:publish:
cmds:
- echo "build:publish"
build:assemble-image:
cmds:
- ../gradlew ${GRADLE_OPTIONS} bootBuildImage
schedule:jacoco:
cmds:
- ../gradlew ${GRADLE_OPTIONS} test jacocoTestReport
|
taskfile.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
name: build-check-task
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
context-directory:
type: string
default: '.'
setup-java-version:
type: string
setup-java-cache:
type: string
setup-node-version:
type: string
setup-pnpm-version:
type: string
setup-npmrc:
type: boolean
default: false
setup-gradle:
type: boolean
default: false
setup-maven:
type: boolean
default: false
setup-task:
type: boolean
default: false
setup-install:
type: boolean
default: false
setup-docker:
type: boolean
default: false
setup-google-cloud-sdk:
type: boolean
default: false
node-build-cache-path:
type: string
gar-npm-repository:
type: string
gar-npm-repository-location:
type: string
gar-npm-repository-scope:
type: string
gcp-workload-identity-provider:
type: string
gcp-service-account-gar:
type: string
gar-host:
type: string
check-tasks:
type: string
required: true
check-tasks-reports:
type: string
default: '{}'
secrets:
workflows-token:
sonar-token:
jobs:
check:
runs-on: ubuntu-24.04
permissions:
contents: 'read'
id-token: 'write'
strategy:
matrix:
task: ${{ fromJSON(inputs.check-tasks) }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup GitHub Actions workflows
id: setup-github-actions-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
context-directory: ${{ inputs.context-directory }}
setup-java-version: ${{ inputs.setup-java-version }}
setup-java-cache: ${{ inputs.setup-java-cache }}
setup-node-version: ${{ inputs.setup-node-version }}
setup-pnpm-version: ${{ inputs.setup-pnpm-version }}
setup-gradle: ${{ inputs.setup-gradle }}
setup-maven: ${{ inputs.setup-maven }}
setup-task: ${{ inputs.setup-task }}
setup-docker: ${{ inputs.setup-docker }}
setup-install: ${{ inputs.setup-install }}
setup-npmrc: ${{ inputs.setup-npmrc }}
setup-google-cloud-sdk: ${{ inputs.setup-google-cloud-sdk }}
node-build-cache-path: ${{ inputs.node-build-cache-path }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-npm-repository: ${{ inputs.gar-npm-repository || vars.GAR_NPM_REPOSITORY }}
gar-npm-repository-location: ${{ inputs.gar-npm-repository-location || vars.GAR_NPM_REPOSITORY_LOCATION }}
gar-npm-repository-scope: ${{ inputs.gar-npm-repository-scope || vars.GAR_NPM_REPOSITORY_SCOPE }}
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gar || vars.GCP_SERVICE_ACCOUNT_GAR }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
token: ${{ secrets.workflows-token }}
- name: Task
id: task
uses: ./.github/actions/task
env:
SONAR_TOKEN: ${{ secrets.sonar-token }}
GITHUB_TOKEN: ${{ secrets.workflows-token }}
with:
context-directory: ${{ inputs.context-directory }}
task: ${{ matrix.task }}
- name: Upload reports
id: upload-reports
uses: ./.github/actions/upload-reports
with:
name: ${{ matrix.task }}
reports: ${{ fromJson(inputs.check-tasks-reports)[matrix.task] }}
reports-on-failure: true
|
.github/workflows/build-check-task.yml
El workflow de la fase de release
En realidad no hay un único workflow de release sino que hay varios para segregar en los aspectos principales y no incluir demasiados condicionales. Hay un workflow de release que genera una imagen de contenedor, genera la release utilizando tareas de Task y genera como resultado un paquete de NPM. El artefacto generado es subido al repositorio de artefactos.
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
name: build-release-dockerfile
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
context-directory:
type: string
default: '.'
builder:
type: string
required: true
bump:
type: string
default: 'patch'
setup-java-version:
type: string
setup-java-cache:
type: string
setup-node-version:
type: string
setup-pnpm-version:
type: string
setup-npmrc:
type: boolean
default: false
setup-gradle:
type: boolean
default: false
setup-maven:
type: boolean
default: false
setup-install:
type: boolean
default: false
setup-task:
type: boolean
default: false
setup-docker:
type: boolean
default: false
setup-google-cloud-sdk:
type: boolean
default: false
gcp-workload-identity-provider:
type: string
gcp-service-account-gar:
type: string
gar-host:
type: string
gar-project:
type: string
gar-docker-repository:
type: string
gar-npm-repository:
type: string
gar-npm-repository-location:
type: string
gar-npm-repository-scope:
type: string
artifact:
type: string
required: false
artifact-name:
type: string
required: false
container-image-name:
type: string
required: true
artifact-dockerfile:
type: string
required: true
setup-crowdin:
type: boolean
default: false
crowdin-project-id:
type: string
outputs:
issue-id:
value: ${{ jobs.release.outputs.issue-id }}
issue-summary:
value: ${{ jobs.release.outputs.issue-summary }}
repository:
value: ${{ jobs.release.outputs.repository }}
pull-request:
value: ${{ jobs.release.outputs.pull-request }}
pull-request-number:
value: ${{ jobs.release.outputs.pull-request-number }}
pull-request-branch:
value: ${{ jobs.release.outputs.pull-request-branch }}
last-version:
value: ${{ jobs.release.outputs.last-version }}
next-version:
value: ${{ jobs.release.outputs.next-version }}
version:
value: ${{ jobs.release.outputs.version }}
tag:
value: ${{ jobs.release.outputs.tag }}
bump:
value: ${{ jobs.release.outputs.bump }}
commit-hash:
value: ${{ jobs.release.outputs.commit-hash }}
branch:
value: ${{ jobs.release.outputs.branch }}
build-number:
value: ${{ jobs.release.outputs.build-number }}
build-date:
value: ${{ jobs.release.outputs.build-date }}
changes:
value: ${{ jobs.release.outputs.changes }}
image-id:
value: ${{ jobs.release.outputs.image-id }}
secrets:
workflows-token:
required: true
jira-token:
required: false
crowdin-personal-token:
required: false
jobs:
release:
runs-on: ubuntu-24.04
outputs:
repository: ${{ steps.build-information.outputs.repository }}
issue-id: ${{ steps.build-information.outputs.issue-id }}
issue-summary: ${{ steps.build-information.outputs.issue-summary }}
pull-request: ${{ steps.build-information.outputs.pull-request }}
pull-request-number: ${{ steps.build-information.outputs.pull-request-number }}
pull-request-branch: ${{ steps.build-information.outputs.pull-request-branch }}
version: ${{ steps.build-information.outputs.version }}
tag: ${{ steps.build-information.outputs.tag }}
bump: ${{ steps.build-information.outputs.bump }}
commit-hash: ${{ steps.build-information.outputs.commit-hash }}
branch: ${{ steps.build-information.outputs.branch }}
build-number: ${{ steps.build-information.outputs.build-number }}
build-date: ${{ steps.build-information.outputs.build-date }}
changes: ${{ steps.build-release-github.outputs.changes }}
image-id: ${{ steps.release-dockerfile.outputs.image-id }}
permissions:
contents: 'write'
id-token: 'write'
pull-requests: 'read'
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup GitHub Actions workflows
id: setup-github-actions-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
context-directory: ${{ inputs.context-directory }}
setup-java-version: ${{ inputs.setup-java-version }}
setup-java-cache: ${{ inputs.setup-java-cache }}
setup-node-version: ${{ inputs.setup-node-version }}
setup-pnpm-version: ${{ inputs.setup-pnpm-version }}
setup-gradle: ${{ inputs.setup-gradle }}
setup-maven: ${{ inputs.setup-maven }}
setup-task: ${{ inputs.setup-task }}
setup-docker: ${{ inputs.setup-docker }}
setup-install: ${{ inputs.setup-install }}
setup-npmrc: ${{ inputs.setup-npmrc }}
setup-google-cloud-sdk: ${{ inputs.setup-google-cloud-sdk }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-npm-repository: ${{ inputs.gar-npm-repository || vars.GAR_NPM_REPOSITORY }}
gar-npm-repository-location: ${{ inputs.gar-npm-repository-location || vars.GAR_NPM_REPOSITORY_LOCATION }}
gar-npm-repository-scope: ${{ inputs.gar-npm-repository-scope || vars.GAR_NPM_REPOSITORY_SCOPE }}
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gar || vars.GCP_SERVICE_ACCOUNT_GAR }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
- name: Configure git
id: configure-git
uses: ./.github/actions/configure-git
- name: Build information
id: build-information
uses: ./.github/actions/build-information
with:
context-directory: ${{ inputs.context-directory }}
github-token: ${{ secrets.GITHUB_TOKEN }}
jira-token: ${{ secrets.jira-token }}
builder: ${{ inputs.builder }}
bump: ${{ inputs.bump }}
- name: Build release git
id: build-release-git
uses: ./.github/actions/build-release-git
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' }}
with:
context-directory: ${{ inputs.context-directory }}
builder: ${{ inputs.builder }}
bump: ${{ steps.build-information.outputs.bump }}
bump-default: ${{ inputs.bump }}
perform: true
- name: Build release github
id: build-release-github
uses: ./.github/actions/build-release-github
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' }}
with:
tag: ${{ steps.build-information.outputs.release-tag }}
- name: Build release artifact
id: build-release-artifact
uses: ./.github/actions/build-release-artifact
with:
context-directory: ${{ inputs.context-directory }}
is-release-branch: ${{ steps.build-information.outputs.is-release-branch }}
tag: ${{ steps.build-information.outputs.release-tag }}
- name: Build release dockerfile
id: release-dockerfile
uses: ./.github/actions/build-release-dockerfile
with:
artifact-dockerfile: ${{ inputs.artifact-dockerfile }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-docker-repository: ${{ inputs.gar-docker-repository || vars.GAR_DOCKER_REPOSITORY }}
artifact: ${{ inputs.artifact }}
artifact-name: ${{ inputs.artifact-name }}
container-image-name: ${{ inputs.container-image-name }}
commit-hash: ${{ steps.build-information.outputs.commit-hash }}
branch: ${{ steps.build-information.outputs.branch }}
version: ${{ steps.build-information.outputs.version }}
builder: ${{ inputs.builder }}
- name: Crowdin upload
id: crowdin-upload
uses: ./.github/actions/crowdin-upload
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' && inputs.setup-crowdin == 'true' }}
with:
crowdin-project-id: ${{ inputs.crowdin-project-id }}
crowdin-personal-token: ${{ secrets.crowdin-personal-token }}
|
.github/workflows/build-release-dockerfile.yml
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
|
FROM ubuntu:24.04 AS build
ARG ARTIFACT
ARG ARTIFACT_NAME
RUN apt update \
&& apt install unzip
RUN mkdir /app/
COPY ${ARTIFACT} /app/
RUN unzip -d /app/ /app/${ARTIFACT_NAME}-*.zip \
&& rm /app/${ARTIFACT_NAME}-*.zip \
&& mv /app/${ARTIFACT_NAME}-* /app/${ARTIFACT_NAME}
RUN ls -l /app/
FROM eclipse-temurin:21-jdk
ARG USERNAME=stubhub
ARG GROUP_NAME=${USERNAME}
ARG USER_UID=2000
ARG USER_GID=${USER_UID}
RUN groupadd --gid ${USER_GID} ${GROUP_NAME} \
&& useradd --uid ${USER_UID} --gid ${USER_GID} ${USERNAME}
RUN mkdir /app/
COPY --from=build /app/ /app/
USER ${USERNAME}
EXPOSE 8080
CMD ["/app/platform-github-actions-workflows-gradle-app/bin/app"]
|
miscellaneous/docker/Dockerfile
El workflow de la fase de deploy
Al igual que el workflow de release no se compone de un único workflow sino de varios para segregar en función del runtime de ejecución, segregar en varios workflows tiene la ventaja de que la cantidad de inputs que recibe son menores lo que facilita su uso y complejidad. La fase de deploy ha de desplegar el artefacto en el entorno de ejecución o runtime que necesite el servicio, puede ser un servicio que hace uso de GKE, de GAE o un Google Function. Hay dos entornos el de desarrollo y el de producción, con reglas para requerir aprobaciones para hacer el despliegue que se han de configurar en cada repositorio.
Varios de los siguientes steps del workflow tienen if condicionales en función de sus condiciones. Añadir un nuevo entorno de ejecución sería añadir un nuevo workflow con sus steps específicos para ese entorno y varios steps iguales que el resto de entorno. Tanto el workflow de build como el de deploy reciben una buena cantidad de parámetros con la que configurar el comportamiento del workflow.
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
name: deploy-gke
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
dry-run:
type: string
gcp-workload-identity-provider:
type: string
gcp-service-account-gke:
type: string
gar-host:
type: string
gar-project:
type: string
gar-docker-repository:
type: string
gke-project:
type: string
deployment-directory:
type: string
container-image-name:
type: string
slack-webhook-url:
type: string
environment:
type: string
release-outputs:
type: string
default: '{}'
build-issue-id:
type: string
build-issue-summary:
type: string
build-repository:
type: string
build-pull-request:
type: string
build-pull-request-number:
type: string
build-pull-request-branch:
type: string
build-version:
type: string
build-tag:
type: string
build-commit-hash:
type: string
build-branch:
type: string
build-number:
type: string
build-date:
type: string
secrets:
workflows-token:
slack-webhook-url:
required: true
jobs:
deploy-gke:
runs-on: ubuntu-22.04-4core
name: gke (${{ inputs.environment }})
environment: ${{ inputs.environment }}
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
fetch-depth: 0
- name: Setup workflows
id: setup-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Summary deprecated inputs
id: summary-deprecated-inputs
uses: ./.github/actions/summary-deprecated-inputs
with:
inputs: ${{ toJSON(inputs) }}
validations: |
{
"build-issue-id": {"validation": "deprecated", "use": "release-outputs"},
"build-issue-summary": {"validation": "deprecated", "use": "release-outputs"},
"build-repository": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request-number": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request-branch": {"validation": "deprecated", "use": "release-outputs"},
"build-version": {"validation": "deprecated", "use": "release-outputs"},
"build-tag": {"validation": "deprecated", "use": "release-outputs"},
"build-commit-hash": {"validation": "deprecated", "use": "release-outputs"},
"build-branch": {"validation": "deprecated", "use": "release-outputs"},
"build-number": {"validation": "deprecated", "use": "release-outputs"},
"build-date": {"validation": "deprecated", "use": "release-outputs"}
}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gke || vars.GCP_SERVICE_ACCOUNT_GKE }}
- name: Deploy GKE
id: deploy-gke
uses: ./.github/actions/deploy-gke
with:
dry-run: ${{ inputs.dry-run }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-docker-repository: ${{ inputs.gar-docker-repository || vars.GAR_DOCKER_REPOSITORY }}
project-id: ${{ inputs.gke-project || vars.GKE_PROJECT }}
deployment-directory: ${{ inputs.deployment-directory }}
container-image-name: ${{ inputs.container-image-name }}
version: ${{ inputs.build-version || fromJSON(inputs.release-outputs).build-version}}
- name: Deploy notification Slack
id: deploy-notification-slack
uses: ./.github/actions/deploy-notification-slack
if: ${{ always() }}
with:
dry-run: ${{ inputs.dry-run }}
ref: ${{ inputs.ref }}
slack-webhook-url: ${{ secrets.slack-webhook-url }}
job-steps: ${{ toJSON(steps) }}
environment: ${{ inputs.environment }}
runtime: gke
gcp-project: ${{ inputs.gke-project || vars.GKE_PROJECT }}
issue-id: ${{ inputs.build-issue-id || fromJSON(inputs.release-outputs).issue-id }}
issue-summary: ${{ inputs.build-issue-summary || fromJSON(inputs.release-outputs).issue-summary }}
repository: ${{ inputs.build-repository || fromJSON(inputs.release-outputs).repository }}
pull-request-number: ${{ inputs.build-pull-request-number || inputs.build-pull-request || fromJSON(inputs.release-outputs).pull-request-number }}
pull-request-branch: ${{ inputs.build-pull-request-branch || fromJSON(inputs.release-outputs).pull-request-branch }}
version: ${{ inputs.build-version || fromJSON(inputs.release-outputs).version }}
tag: ${{ inputs.build-tag || fromJSON(inputs.release-outputs).tag || format('v{0}', inputs.build-version) }}
commit-hash: ${{ inputs.build-commit-hash || fromJSON(inputs.release-outputs).commit-hash }}
branch: ${{ inputs.build-branch || fromJSON(inputs.release-outputs).branch }}
build-number: ${{ inputs.build-number || fromJSON(inputs.release-outputs).build-number }}
build-date: ${{ inputs.build-date || fromJSON(inputs.release-outputs).build-date }}
|
github/workflows/deploy-gke.yml
El workflow reutilizable del CI/CD
Estos son los workflows reutilizables del pipeline, cada repositorio tiene que definir un workflow que los invoque y proporcione los argumentos necesarios, en realidad los workflows que define cada repositorio son simplemente una colección de propiedades proporcionados a los workflows reutilizables. Este puede ser uno de ejemplo para un proyecto de Java que construye una imagen de Docker con un Dockerfile, lo publica en Google Artifact Repository y lo despliega en GKE.
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
|
name: main
on:
push:
branches:
- main
- master
merge_group:
workflow_dispatch:
inputs:
ref:
description: 'Reference to checkout'
required: true
type: string
jobs:
build:
name: build
uses: ./.github/workflows/build-check-task.yml
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
context-directory: ./gradle-app
setup-java-version: ${{ vars.JAVA_VERSION }}
setup-gradle: true
setup-task: true
setup-docker: true
check-tasks: |
["build:test", "build:pmd", "build:checkstyle", "build:spotbugs", "build:sonar"]
check-tasks-reports: |
{
"build:test": "./gradle-app/build/reports/tests",
"build:pmd": "./gradle-app/build/reports/pmd",
"build:checkstyle": "./gradle-app/build/reports/checkstyle",
"build:spotbugs": "./gradle-app/build/spotbugs"
}
secrets:
workflows-token: ${{ secrets.GH_CI_TOKEN }}
sonar-token: ${{ secrets.SONAR_TOKEN }}
release:
name: release
uses: ./.github/workflows/build-release-dockerfile.yml
needs: [build]
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
context-directory: ./gradle-app
builder: gradle
bump: patch
setup-java-version: ${{ vars.JAVA_VERSION }}
setup-gradle: true
setup-task: true
setup-docker: true
artifact: ./gradle-app/build/distributions/platform-github-actions-workflows-*.zip
artifact-name: platform-github-actions-workflows-gradle-app
container-image-name: platform-github-actions-workflows
artifact-dockerfile: ./gradle-app/miscellaneous/docker/Dockerfile
secrets:
workflows-token: ${{ secrets.GH_CI_TOKEN }}
jira-token: ${{ secrets.JIRA_TOKEN }}
deploy:
name: deploy
uses: ./.github/workflows/deploy-gke.yml
needs: [release]
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
dry-run: true
deployment-directory: ./gradle-app/miscellaneous/gke/development
container-image-name: platform-github-actions-workflows
environment: production
release-outputs: ${{ toJSON(needs.release.outputs) }}
secrets:
workflows-token: ${{ secrets.GH_CI_TOKEN }}
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
.github/workflows/main.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
name: build-check-task
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
context-directory:
type: string
default: '.'
setup-java-version:
type: string
setup-java-cache:
type: string
setup-node-version:
type: string
setup-pnpm-version:
type: string
setup-npmrc:
type: boolean
default: false
setup-gradle:
type: boolean
default: false
setup-maven:
type: boolean
default: false
setup-task:
type: boolean
default: false
setup-install:
type: boolean
default: false
setup-docker:
type: boolean
default: false
setup-google-cloud-sdk:
type: boolean
default: false
node-build-cache-path:
type: string
gar-npm-repository:
type: string
gar-npm-repository-location:
type: string
gar-npm-repository-scope:
type: string
gcp-workload-identity-provider:
type: string
gcp-service-account-gar:
type: string
gar-host:
type: string
check-tasks:
type: string
required: true
check-tasks-reports:
type: string
default: '{}'
secrets:
workflows-token:
sonar-token:
jobs:
check:
runs-on: ubuntu-24.04
permissions:
contents: 'read'
id-token: 'write'
strategy:
matrix:
task: ${{ fromJSON(inputs.check-tasks) }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup GitHub Actions workflows
id: setup-github-actions-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
context-directory: ${{ inputs.context-directory }}
setup-java-version: ${{ inputs.setup-java-version }}
setup-java-cache: ${{ inputs.setup-java-cache }}
setup-node-version: ${{ inputs.setup-node-version }}
setup-pnpm-version: ${{ inputs.setup-pnpm-version }}
setup-gradle: ${{ inputs.setup-gradle }}
setup-maven: ${{ inputs.setup-maven }}
setup-task: ${{ inputs.setup-task }}
setup-docker: ${{ inputs.setup-docker }}
setup-install: ${{ inputs.setup-install }}
setup-npmrc: ${{ inputs.setup-npmrc }}
setup-google-cloud-sdk: ${{ inputs.setup-google-cloud-sdk }}
node-build-cache-path: ${{ inputs.node-build-cache-path }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-npm-repository: ${{ inputs.gar-npm-repository || vars.GAR_NPM_REPOSITORY }}
gar-npm-repository-location: ${{ inputs.gar-npm-repository-location || vars.GAR_NPM_REPOSITORY_LOCATION }}
gar-npm-repository-scope: ${{ inputs.gar-npm-repository-scope || vars.GAR_NPM_REPOSITORY_SCOPE }}
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gar || vars.GCP_SERVICE_ACCOUNT_GAR }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
token: ${{ secrets.workflows-token }}
- name: Task
id: task
uses: ./.github/actions/task
env:
SONAR_TOKEN: ${{ secrets.sonar-token }}
GITHUB_TOKEN: ${{ secrets.workflows-token }}
with:
context-directory: ${{ inputs.context-directory }}
task: ${{ matrix.task }}
- name: Upload reports
id: upload-reports
uses: ./.github/actions/upload-reports
with:
name: ${{ matrix.task }}
reports: ${{ fromJson(inputs.check-tasks-reports)[matrix.task] }}
reports-on-failure: true
|
.github/workflows/build-check-task.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
name: build-release-dockerfile
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
context-directory:
type: string
default: '.'
builder:
type: string
required: true
bump:
type: string
default: 'patch'
setup-java-version:
type: string
setup-java-cache:
type: string
setup-node-version:
type: string
setup-pnpm-version:
type: string
setup-npmrc:
type: boolean
default: false
setup-gradle:
type: boolean
default: false
setup-maven:
type: boolean
default: false
setup-install:
type: boolean
default: false
setup-task:
type: boolean
default: false
setup-docker:
type: boolean
default: false
setup-google-cloud-sdk:
type: boolean
default: false
gcp-workload-identity-provider:
type: string
gcp-service-account-gar:
type: string
gar-host:
type: string
gar-project:
type: string
gar-docker-repository:
type: string
gar-npm-repository:
type: string
gar-npm-repository-location:
type: string
gar-npm-repository-scope:
type: string
artifact:
type: string
required: false
artifact-name:
type: string
required: false
container-image-name:
type: string
required: true
artifact-dockerfile:
type: string
required: true
setup-crowdin:
type: boolean
default: false
crowdin-project-id:
type: string
outputs:
issue-id:
value: ${{ jobs.release.outputs.issue-id }}
issue-summary:
value: ${{ jobs.release.outputs.issue-summary }}
repository:
value: ${{ jobs.release.outputs.repository }}
pull-request:
value: ${{ jobs.release.outputs.pull-request }}
pull-request-number:
value: ${{ jobs.release.outputs.pull-request-number }}
pull-request-branch:
value: ${{ jobs.release.outputs.pull-request-branch }}
last-version:
value: ${{ jobs.release.outputs.last-version }}
next-version:
value: ${{ jobs.release.outputs.next-version }}
version:
value: ${{ jobs.release.outputs.version }}
tag:
value: ${{ jobs.release.outputs.tag }}
bump:
value: ${{ jobs.release.outputs.bump }}
commit-hash:
value: ${{ jobs.release.outputs.commit-hash }}
branch:
value: ${{ jobs.release.outputs.branch }}
build-number:
value: ${{ jobs.release.outputs.build-number }}
build-date:
value: ${{ jobs.release.outputs.build-date }}
changes:
value: ${{ jobs.release.outputs.changes }}
image-id:
value: ${{ jobs.release.outputs.image-id }}
secrets:
workflows-token:
required: true
jira-token:
required: false
crowdin-personal-token:
required: false
jobs:
release:
runs-on: ubuntu-24.04
outputs:
repository: ${{ steps.build-information.outputs.repository }}
issue-id: ${{ steps.build-information.outputs.issue-id }}
issue-summary: ${{ steps.build-information.outputs.issue-summary }}
pull-request: ${{ steps.build-information.outputs.pull-request }}
pull-request-number: ${{ steps.build-information.outputs.pull-request-number }}
pull-request-branch: ${{ steps.build-information.outputs.pull-request-branch }}
version: ${{ steps.build-information.outputs.version }}
tag: ${{ steps.build-information.outputs.tag }}
bump: ${{ steps.build-information.outputs.bump }}
commit-hash: ${{ steps.build-information.outputs.commit-hash }}
branch: ${{ steps.build-information.outputs.branch }}
build-number: ${{ steps.build-information.outputs.build-number }}
build-date: ${{ steps.build-information.outputs.build-date }}
changes: ${{ steps.build-release-github.outputs.changes }}
image-id: ${{ steps.release-dockerfile.outputs.image-id }}
permissions:
contents: 'write'
id-token: 'write'
pull-requests: 'read'
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup GitHub Actions workflows
id: setup-github-actions-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
context-directory: ${{ inputs.context-directory }}
setup-java-version: ${{ inputs.setup-java-version }}
setup-java-cache: ${{ inputs.setup-java-cache }}
setup-node-version: ${{ inputs.setup-node-version }}
setup-pnpm-version: ${{ inputs.setup-pnpm-version }}
setup-gradle: ${{ inputs.setup-gradle }}
setup-maven: ${{ inputs.setup-maven }}
setup-task: ${{ inputs.setup-task }}
setup-docker: ${{ inputs.setup-docker }}
setup-install: ${{ inputs.setup-install }}
setup-npmrc: ${{ inputs.setup-npmrc }}
setup-google-cloud-sdk: ${{ inputs.setup-google-cloud-sdk }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-npm-repository: ${{ inputs.gar-npm-repository || vars.GAR_NPM_REPOSITORY }}
gar-npm-repository-location: ${{ inputs.gar-npm-repository-location || vars.GAR_NPM_REPOSITORY_LOCATION }}
gar-npm-repository-scope: ${{ inputs.gar-npm-repository-scope || vars.GAR_NPM_REPOSITORY_SCOPE }}
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gar || vars.GCP_SERVICE_ACCOUNT_GAR }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
- name: Configure git
id: configure-git
uses: ./.github/actions/configure-git
- name: Build information
id: build-information
uses: ./.github/actions/build-information
with:
context-directory: ${{ inputs.context-directory }}
github-token: ${{ secrets.GITHUB_TOKEN }}
jira-token: ${{ secrets.jira-token }}
builder: ${{ inputs.builder }}
bump: ${{ inputs.bump }}
- name: Build release git
id: build-release-git
uses: ./.github/actions/build-release-git
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' }}
with:
context-directory: ${{ inputs.context-directory }}
builder: ${{ inputs.builder }}
bump: ${{ steps.build-information.outputs.bump }}
bump-default: ${{ inputs.bump }}
perform: true
- name: Build release github
id: build-release-github
uses: ./.github/actions/build-release-github
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' }}
with:
tag: ${{ steps.build-information.outputs.release-tag }}
- name: Build release artifact
id: build-release-artifact
uses: ./.github/actions/build-release-artifact
with:
context-directory: ${{ inputs.context-directory }}
is-release-branch: ${{ steps.build-information.outputs.is-release-branch }}
tag: ${{ steps.build-information.outputs.release-tag }}
- name: Build release dockerfile
id: release-dockerfile
uses: ./.github/actions/build-release-dockerfile
with:
artifact-dockerfile: ${{ inputs.artifact-dockerfile }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-docker-repository: ${{ inputs.gar-docker-repository || vars.GAR_DOCKER_REPOSITORY }}
artifact: ${{ inputs.artifact }}
artifact-name: ${{ inputs.artifact-name }}
container-image-name: ${{ inputs.container-image-name }}
commit-hash: ${{ steps.build-information.outputs.commit-hash }}
branch: ${{ steps.build-information.outputs.branch }}
version: ${{ steps.build-information.outputs.version }}
builder: ${{ inputs.builder }}
- name: Crowdin upload
id: crowdin-upload
uses: ./.github/actions/crowdin-upload
if: ${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' && inputs.setup-crowdin == 'true' }}
with:
crowdin-project-id: ${{ inputs.crowdin-project-id }}
crowdin-personal-token: ${{ secrets.crowdin-personal-token }}
|
.github/workflows/build-release-dockerfile.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
name: deploy-gke
on:
workflow_call:
inputs:
ref:
type: string
default: 'main'
workflows-ref:
type: string
default: 'main'
dry-run:
type: string
gcp-workload-identity-provider:
type: string
gcp-service-account-gke:
type: string
gar-host:
type: string
gar-project:
type: string
gar-docker-repository:
type: string
gke-project:
type: string
deployment-directory:
type: string
container-image-name:
type: string
slack-webhook-url:
type: string
environment:
type: string
release-outputs:
type: string
default: '{}'
build-issue-id:
type: string
build-issue-summary:
type: string
build-repository:
type: string
build-pull-request:
type: string
build-pull-request-number:
type: string
build-pull-request-branch:
type: string
build-version:
type: string
build-tag:
type: string
build-commit-hash:
type: string
build-branch:
type: string
build-number:
type: string
build-date:
type: string
secrets:
workflows-token:
slack-webhook-url:
required: true
jobs:
deploy-gke:
runs-on: ubuntu-22.04-4core
name: gke (${{ inputs.environment }})
environment: ${{ inputs.environment }}
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.workflows-token }}
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
fetch-depth: 0
- name: Setup workflows
id: setup-workflows
uses: acmehub/platform-github-actions-workflows/.github/actions/setup-github-actions-workflows@main
if: ${{ github.repository != 'acmehub/platform-github-actions-workflows' }}
with:
ref: ${{ inputs.workflows-ref }}
token: ${{ secrets.workflows-token }}
- name: Summary deprecated inputs
id: summary-deprecated-inputs
uses: ./.github/actions/summary-deprecated-inputs
with:
inputs: ${{ toJSON(inputs) }}
validations: |
{
"build-issue-id": {"validation": "deprecated", "use": "release-outputs"},
"build-issue-summary": {"validation": "deprecated", "use": "release-outputs"},
"build-repository": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request-number": {"validation": "deprecated", "use": "release-outputs"},
"build-pull-request-branch": {"validation": "deprecated", "use": "release-outputs"},
"build-version": {"validation": "deprecated", "use": "release-outputs"},
"build-tag": {"validation": "deprecated", "use": "release-outputs"},
"build-commit-hash": {"validation": "deprecated", "use": "release-outputs"},
"build-branch": {"validation": "deprecated", "use": "release-outputs"},
"build-number": {"validation": "deprecated", "use": "release-outputs"},
"build-date": {"validation": "deprecated", "use": "release-outputs"}
}
- name: Setup meta
id: setup-meta
uses: ./.github/actions/setup-meta
with:
gcp-workload-identity-provider: ${{ inputs.gcp-workload-identity-provider || vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
gcp-service-account: ${{ inputs.gcp-service-account-gke || vars.GCP_SERVICE_ACCOUNT_GKE }}
- name: Deploy GKE
id: deploy-gke
uses: ./.github/actions/deploy-gke
with:
dry-run: ${{ inputs.dry-run }}
gar-host: ${{ inputs.gar-host || vars.GAR_HOST }}
gar-project: ${{ inputs.gar-project || vars.GAR_PROJECT }}
gar-docker-repository: ${{ inputs.gar-docker-repository || vars.GAR_DOCKER_REPOSITORY }}
project-id: ${{ inputs.gke-project || vars.GKE_PROJECT }}
deployment-directory: ${{ inputs.deployment-directory }}
container-image-name: ${{ inputs.container-image-name }}
version: ${{ inputs.build-version || fromJSON(inputs.release-outputs).build-version}}
- name: Deploy notification Slack
id: deploy-notification-slack
uses: ./.github/actions/deploy-notification-slack
if: ${{ always() }}
with:
dry-run: ${{ inputs.dry-run }}
ref: ${{ inputs.ref }}
slack-webhook-url: ${{ secrets.slack-webhook-url }}
job-steps: ${{ toJSON(steps) }}
environment: ${{ inputs.environment }}
runtime: gke
gcp-project: ${{ inputs.gke-project || vars.GKE_PROJECT }}
issue-id: ${{ inputs.build-issue-id || fromJSON(inputs.release-outputs).issue-id }}
issue-summary: ${{ inputs.build-issue-summary || fromJSON(inputs.release-outputs).issue-summary }}
repository: ${{ inputs.build-repository || fromJSON(inputs.release-outputs).repository }}
pull-request-number: ${{ inputs.build-pull-request-number || inputs.build-pull-request || fromJSON(inputs.release-outputs).pull-request-number }}
pull-request-branch: ${{ inputs.build-pull-request-branch || fromJSON(inputs.release-outputs).pull-request-branch }}
version: ${{ inputs.build-version || fromJSON(inputs.release-outputs).version }}
tag: ${{ inputs.build-tag || fromJSON(inputs.release-outputs).tag || format('v{0}', inputs.build-version) }}
commit-hash: ${{ inputs.build-commit-hash || fromJSON(inputs.release-outputs).commit-hash }}
branch: ${{ inputs.build-branch || fromJSON(inputs.release-outputs).branch }}
build-number: ${{ inputs.build-number || fromJSON(inputs.release-outputs).build-number }}
build-date: ${{ inputs.build-date || fromJSON(inputs.release-outputs).build-date }}
|
.github/workflows/deploy-gke.yml
Las acciones de soporte para los workflows del CI/CD
Para reutilizar funciones en varios jobs el pipeline incluye algunas acciones también reutilizables, para configurar Gradle, Node, Task, Google Cloud y notificaciones de Slack entre otras.
Un aspecto importante es que como regla los repositorios de los servicios solo han de usar workflows reutilizables no actions. Esta regla tiene el objetivo facilitar el mantenimiento de los workflows y poder hacer cambios manteniendo la compatibilidad hacia atrás.
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
name: build-information
description: 'Build information'
inputs:
context-directory:
type: string
required: false
default: '.'
github-token:
type: string
required: true
jira-token:
type: string
required: true
builder:
type: string
required: true
bump:
type: string
required: true
outputs:
repository:
value: ${{ steps.build-information.outputs.repository }}
issue-id:
value: ${{ steps.build-information-issue-id.outputs.issue-id }}
issue-summary:
value: ${{ steps.build-information-issue-summary.outputs.issue-summary }}
pull-request:
value: ${{ steps.build-information-pull-request-outputs.outputs.pull-request-number }}
pull-request-number:
value: ${{ steps.build-information-pull-request-outputs.outputs.pull-request-number }}
pull-request-branch:
value: ${{ steps.build-information-pull-request-outputs.outputs.pull-request-branch }}
commit-hash:
value: ${{ steps.build-information.outputs.commit-hash }}
commit-hash-long:
value: ${{ steps.build-information.outputs.commit-hash-long }}
branch:
value: ${{ steps.build-information.outputs.branch }}
build-number:
value: ${{ steps.build-information.outputs.build-number }}
build-date:
value: ${{ steps.build-information.outputs.build-date }}
is-release-branch:
value: ${{ steps.build-information-release.outputs.is-release-branch }}
version:
value: ${{ steps.build-information-release.outputs.version }}
tag:
value: ${{ steps.build-information-release.outputs.tag }}
bump:
value: ${{ steps.build-information-bump.outputs.bump }}
current-version:
value: ${{ steps.build-information-release.outputs.current-version }}
release-version:
value: ${{ steps.build-information-release.outputs.release-version }}
release-tag:
value: ${{ steps.build-information-release.outputs.release-tag }}
next-version:
value: ${{ steps.build-information-release.outputs.next-version }}
runs:
using: 'composite'
steps:
- name: Build information
id: build-information
shell: bash
run: |-
OWNER="${{ github.repository_owner }}"
REPOSITORY=$(echo "${{ github.repository }}" | sed -e "s/${OWNER}\///g")
COMMIT_HASH="$(git rev-parse --short HEAD)"
COMMIT_HASH_LONG="$(git rev-parse HEAD)"
BRANCH="$(git branch --show-current)"
BUILD_NUMBER="${{ github.run_number }}"
BUILD_DATE="$(date +"%Y-%m-%dT%H-%M-%S")"
echo "Repository: ${REPOSITORY}"
echo "Commit HASH: ${COMMIT_HASH}"
echo "Commit HASH (long): ${COMMIT_HASH_LONG}"
echo "Branch: ${BRANCH}"
echo "Build number: ${BUILD_NUMBER}"
echo "Build date: ${BUILD_DATE}"
echo "repository=${REPOSITORY}" >> $GITHUB_OUTPUT
echo "commit-hash=${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "commit-hash-long=${COMMIT_HASH_LONG}" >> $GITHUB_OUTPUT
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "build-number=${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "build-date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Build information pull request
id: build-information-pull-request
uses: actions/github-script@v7
with:
result-encoding: string
github-token: ${{ inputs.github-token }}
script: |-
const owner = "${{ github.repository_owner }}";
const repository = "${{ steps.build-information.outputs.repository }}";
const commitHash = "${{ steps.build-information.outputs.commit-hash-long }}";
let pullRequestNumber = "${{ github.event.pull_request.number }}";
let pullRequestBranch = "";
let pullRequestLabels = [];
if (pullRequestNumber === "") {
const response = await github.rest.pulls.list({
"owner": owner,
"repo": repository,
"state": "closed",
"sort": "updated",
"direction": "desc",
"per_page": 100
});
//console.log("Response: " + JSON.stringify(response));
const pull = response.data.find(p => p.merge_commit_sha === commitHash);
pullRequestNumber = (!pull) ? 0 : pull.number;
}
if (pullRequestNumber) {
const response = await github.rest.pulls.get({
"owner": owner,
"repo": repository,
"pull_number": pullRequestNumber
});
//console.log("Response: " + JSON.stringify(response));
pullRequestBranch = response.data.head.ref;
pullRequestLabels = response.data.labels;
}
console.log(`Pull request number: ${pullRequestNumber}`);
console.log(`Pull request branch: ${pullRequestBranch}`);
console.log(`Pull request labels: ${pullRequestLabels}`);
return JSON.stringify({ "pull-request-number": pullRequestNumber, "pull-request-branch": pullRequestBranch, "pull-request-labels": pullRequestLabels });
- name: Build information pull request outputs
id: build-information-pull-request-outputs
shell: bash
run: |-
PULL_REQUEST_NUMBER="${{ fromJSON(steps.build-information-pull-request.outputs.result).pull-request-number }}"
PULL_REQUEST_BRANCH="${{ fromJSON(steps.build-information-pull-request.outputs.result).pull-request-branch }}"
echo "pull-request-number=${PULL_REQUEST_NUMBER}" >> $GITHUB_OUTPUT
echo "pull-request-branch=${PULL_REQUEST_BRANCH}" >> $GITHUB_OUTPUT
- name: Build information issue id
id: build-information-issue-id
shell: bash
run: |-
ISSUE_CANDIDATE_REGULAR_EXPRESSION="^\([A-Z]\+-[0-9]\+\)\(.*\)"
ISSUE_REGULAR_EXPRESSION="^[A-Z]+-[0-9]+$"
PULL_REQUEST_BRANCH="${{ fromJSON(steps.build-information-pull-request.outputs.result).pull-request-branch }}"
ISSUE_ID=""
ISSUE_CANDIDATE="${PULL_REQUEST_BRANCH}"
ISSUE_CANDIDATE=$(echo "${ISSUE_CANDIDATE}" | sed -e "s#^bug/##")
ISSUE_CANDIDATE=$(echo "${ISSUE_CANDIDATE}" | sed -e "s#^feat/##")
ISSUE_CANDIDATE=$(echo "${ISSUE_CANDIDATE}" | sed -e "s#^breaking/##")
ISSUE_CANDIDATE=$(echo "${ISSUE_CANDIDATE}" | sed -e "s#${ISSUE_CANDIDATE_REGULAR_EXPRESSION}#\1#")
if [[ "${ISSUE_CANDIDATE}" =~ ${ISSUE_REGULAR_EXPRESSION} ]]; then
ISSUE_ID="${ISSUE_CANDIDATE}"
fi
echo "Issue: ${ISSUE_ID}"
echo "issue-id=${ISSUE_ID}" >> $GITHUB_OUTPUT
- name: Build information issue summary
id: build-information-issue-summary
if: ${{ steps.build-information-issue-id.outputs.issue-id && inputs.jira-token }}
shell: bash
run: |-
ISSUE_ID="${{ steps.build-information-issue-id.outputs.issue-id }}"
RESPONSE=$(curl -X GET \
-H "Authorization: Basic ${{ inputs.jira-token }}" \
-H "Accept: application/json" \
"https://acmehub.atlassian.net/rest/agile/1.0/issue/${ISSUE_ID}")
ISSUE_SUMMARY=$(echo "${RESPONSE}" | jq --raw-output '.fields.summary')
echo "Issue summary: ${ISSUE_SUMMARY}"
echo "issue-summary=${ISSUE_SUMMARY}" >> $GITHUB_OUTPUT
- name: Build information bump branch
id: build-information-bump-branch
shell: bash
run: |-
BRANCH="${{ fromJSON(steps.build-information-pull-request.outputs.result).pull-request-branch }}"
BUMP=""
if [[ "${BRANCH}" =~ ^bug/.* ]]; then
BUMP="patch"
fi
if [[ "${BRANCH}" =~ ^feat/.* ]]; then
BUMP="minor"
fi
if [[ "${BRANCH}" =~ ^breaking/.* ]]; then
BUMP="major"
fi
echo "Branch bump: ${BUMP}"
echo "bump=${BUMP}" >> $GITHUB_OUTPUT
- name: Build information bump label
id: build-information-bump-label
uses: actions/github-script@v7
if: ${{ steps.build-information-bump-branch.outputs.bump == '' }}
with:
result-encoding: string
github-token: ${{ inputs.github-token }}
script: |-
const pullRequestLabels = JSON.parse(`${{ steps.build-information-pull-request.outputs.result }}`)["pull-request-labels"];
let bump = "";
let patch = pullRequestLabels.find(e => e.name === "bump-patch");
let minor = pullRequestLabels.find(e => e.name === "bump-minor");
let major = pullRequestLabels.find(e => e.name === "bump-major");
if (patch) {
bump = "patch";
}
if (minor) {
bump = "minor";
}
if (major) {
bump = "major";
}
console.log(`Label bump: ${bump}`);
return bump;
- name: Build information bump
id: build-information-bump
shell: bash
run: |-
BRANCH_BUMP="${{ steps.build-information-bump-branch.outputs.bump }}"
LABEL_BUMP="${{ steps.build-information-bump-label.outputs.result }}"
BUMP="${{ inputs.bump }}"
if [ -n "${BRANCH_BUMP}" ]; then
BUMP="${BRANCH_BUMP}"
elif [ -n "${LABEL_BUMP}" ]; then
BUMP="${LABEL_BUMP}"
fi
echo "Branch bump: ${BRANCH_BUMP}"
echo "Label bump: ${LABEL_BUMP}"
echo "Bump: ${BUMP}"
echo "bump=${BUMP}" >> $GITHUB_OUTPUT
- name: Build information release
id: build-information-release
uses: ./.github/actions/build-release-git
with:
context-directory: ${{ inputs.context-directory }}
builder: ${{ inputs.builder }}
bump: ${{ steps.build-information-bump.outputs.bump }}
bump-default: ${{ inputs.bump }}
perform: false
|
.github/actions/build-information/action.yml
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
|
name: build-release-dockerfile
description: 'Build release dockerfile'
inputs:
artifact-dockerfile:
type: string
required: true
gar-host:
type: string
required: true
gar-project:
type: string
required: true
gar-docker-repository:
type: string
required: true
artifact:
type: string
required: false
artifact-name:
type: string
required: false
container-image-name:
type: string
required: true
commit-hash:
type: string
required: true
branch:
type: string
required: true
version:
type: string
required: true
builder:
type: string
required: false
outputs:
image-id:
value: ${{ steps.docker-build-push.outputs.imageid }}
runs:
using: 'composite'
steps:
- name: Sanitize branch name
shell: bash
id: sanitize-branch
run: |
BRANCH="${{ inputs.branch }}"
SANITIZED_BRANCH="${BRANCH//\//-}"
echo "sanitized-branch=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Docker tags
id: docker-image-tags
shell: bash
run: |-
TAGS_ARRAY=()
TAGS_ARRAY+=("${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:${{ inputs.commit-hash }}")
TAGS_ARRAY+=("${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:${{ steps.sanitize-branch.outputs.sanitized-branch }}")
TAGS_ARRAY+=("${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:${{ inputs.version }}")
if [ "${{ contains(fromJSON('["main", "master"]'), github.ref_name) && github.event_name == 'push' }}" == "true" ]; then
TAGS_ARRAY+=("${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:latest")
fi
TAGS=$(IFS=','; echo "${TAGS_ARRAY[*]}" )
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push container image
id: docker-build-push
uses: docker/build-push-action@v5
with:
push: true
context: ./
file: ${{ inputs.artifact-dockerfile }}
tags: ${{ steps.docker-image-tags.outputs.tags }}
build-args: |
ARTIFACT=${{ inputs.artifact }}
ARTIFACT_NAME=${{ inputs.artifact-name }}
|
.github/actions/build-release-dockerfile/action.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
name: build-release-git
description: 'Build release git'
inputs:
dry-run:
type: boolean
default: false
context-directory:
type: string
default: '.'
task-file:
type: string
required: false
default: './taskfile.yml'
builder:
type: string
required: true
bump:
type: string
default: 'patch'
bump-default:
type: string
default: 'patch'
perform:
type: boolean
default: false
outputs:
snapshot-tag:
value: ${{ steps.build-release-git.outputs.snapshot-tag }}
branch:
value: ${{ steps.build-release-git.outputs.branch }}
is-release-branch:
value: ${{ steps.build-release-git.outputs.is-release-branch }}
version:
value: ${{ steps.build-release-git.outputs.version }}
tag:
value: ${{ steps.build-release-git.outputs.tag }}
current-version:
value: ${{ steps.build-release-git.outputs.current-version }}
release-version:
value: ${{ steps.build-release-git.outputs.release-version }}
release-tag:
value: ${{ steps.build-release-git.outputs.release-tag }}
next-version:
value: ${{ steps.build-release-git.outputs.next-version }}
runs:
using: 'composite'
steps:
- name: Build release git
id: build-release-git
shell: bash
working-directory: ./
run: |-
SCRIPT="${{ github.action_path }}/git-release.sh"
OPTIONS=""
TASK_FILE="${{ inputs.task-file }}"
if [ ! -f "${TASK_FILE}" ]; then
TASK_FILE="${{ inputs.context-directory }}/${{ inputs.task-file }}"
fi
if [ ! -f "${TASK_FILE}" ]; then
TASK_FILE=".github/actions/resources/taskfiles/${{ inputs.task-file }}"
fi
if [ "${{ inputs.dry-run }}" == "true" ]; then
OPTIONS="$OPTIONS -d"
fi
OPTIONS="$OPTIONS -c ${{ inputs.context-directory }}"
OPTIONS="$OPTIONS -b ${{ inputs.builder }}"
OPTIONS="$OPTIONS -u ${{ inputs.bump }}"
OPTIONS="$OPTIONS -w ${{ inputs.bump-default }}"
OPTIONS="$OPTIONS -t ${TASK_FILE}"
RELEASE_INFORMATION=$(${SCRIPT} ${OPTIONS} -i)
SNAPSHOT_TAG=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."snapshot-tag"')
BRANCH=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."branch"')
IS_RELEASE_BRANCH=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."is-release-branch"')
VERSION=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."version"')
TAG=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."tag"')
CURRENT_VERSION=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."current-version"')
RELEASE_VERSION=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."release-version"')
RELEASE_TAG=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."release-tag"')
NEXT_VERSION=$(echo "${RELEASE_INFORMATION}" | jq --raw-output '."next-version"')
if [ "${{ inputs.perform }}" == "true" ]; then
${SCRIPT} ${OPTIONS} -p
fi
echo "Snapshot tag: ${SNAPSHOT_TAG}"
echo "Branch: ${BRANCH}"
echo "Is release branch: ${IS_RELEASE_BRANCH}"
echo "Version: ${VERSION}"
echo "Tag: ${TAG}"
echo "Current version: ${CURRENT_VERSION}"
echo "Release version: ${RELEASE_VERSION}"
echo "Release tag: ${RELEASE_TAG}"
echo "Next version: ${NEXT_VERSION}"
echo "snapshot-tag=${SNAPSHOT_TAG}" >> $GITHUB_OUTPUT
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "is-release-branch=${IS_RELEASE_BRANCH}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "current-version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT
echo "release-version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
echo "release-tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
echo "next-version=${NEXT_VERSION}" >> $GITHUB_OUTPUT
|
.github/actions/build-release-git/action.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
|
#!/usr/bin/env bash
set -eu
# Arguments
## builder: gradle, maven, lerna, npm
## bump: major, minor, patch
#
# Usage
## Get release information
# ./releaser.sh -b gradle -u patch -w patch -t "./gradle-app/taskfile.yml" -i
## Perform in dry-run
# ./releaser.sh -b gradle -u patch -w patch -t "./gradle-app/taskfile.yml" -v -d -p
## Perform
# ./releaser.sh -b gradle -u patch -w patch -t "./gradle-app/taskfile.yml" -v -p
# Global config variables
VERSION_PREFIX="v"
DRY_RUN="false"
function get_snapshot_tag() {
local BUILDER="$1"
local SNAPSHOT_TAG=""
if [ "${BUILDER}" == "gradle" ] || [ "${BUILDER}" == "maven" ] || [ "${BUILDER}" == "npm" ]; then
SNAPSHOT_TAG="-SNAPSHOT"
fi
echo "${SNAPSHOT_TAG}"
}
function get_version() {
local TASK_FILE="$1"
local BUILDER="$2"
local VERSION=""
BUILDERS=("gradle" "maven" "npm")
if [[ " ${BUILDERS[*]} " =~ " ${BUILDER} " ]] && [ -z "${VERSION}" ]; then
VERSION=$(task --taskfile "${TASK_FILE}" build:version)
fi
if [ "${BUILDER}" == "lerna" ] && [ -z "${VERSION}" ] ; then
VERSION=$(echo $(date '+%Y%m%d%H%M%S'))
fi
echo "${VERSION}"
}
function set_version() {
local CONTEXT_DIRECTORY="$1"
local BUILDER="$2"
local VERSION="$3"
if [ "${BUILDER}" == "gradle" ]; then
find . -name "gradle.properties" | xargs sed -i "s/^[#]*\s*version=.*/version=${VERSION}/"
fi
if [ "${BUILDER}" == "maven" ]; then
./mvnw versions:set -DnewVersion="${VERSION}"
fi
if [ "${BUILDER}" == "npm" ]; then
npm version "${VERSION}" --no-git-tag-version
fi
}
function parse_version() {
local RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)'
local VERSION="$1"
local COMPONENT="$2"
local RESULT=""
local MAJOR=$(echo "${VERSION}" | sed -e "s#${RE}#\1#")
local MINOR=$(echo "${VERSION}" | sed -e "s#${RE}#\2#")
local PATCH=$(echo "${VERSION}" | sed -e "s#${RE}#\3#")
local TAG=$(echo "${VERSION}" | sed -e "s#${RE}#\4#")
if [ "${COMPONENT}" == "major" ]; then
RESULT=${MAJOR}
fi
if [ "${COMPONENT}" == "minor" ]; then
RESULT=${MINOR}
fi
if [ "${COMPONENT}" == "patch" ]; then
RESULT=${PATCH}
fi
if [ "${COMPONENT}" == "tag" ]; then
RESULT=${TAG}
fi
echo "${RESULT}"
}
function release_version() {
local VERSION="$1"
local BUMP="$2"
if [ "${BUMP}" == "major" ] || [ "${BUMP}" == "minor" ] || [ "${BUMP}" == "patch" ]; then
local VERSION_MAJOR=$(parse_version ${VERSION} "major")
local VERSION_MINOR=$(parse_version ${VERSION} "minor")
local VERSION_PATCH=$(parse_version ${VERSION} "patch")
local VERSION_BUMP=$(next_version "${VERSION}" "${BUMP}" "")
local VERSION_BUMP_MAJOR=$(parse_version ${VERSION_BUMP} "major")
local VERSION_BUMP_MINOR=$(parse_version ${VERSION_BUMP} "minor")
local VERSION_BUMP_PATCH=$(parse_version ${VERSION_BUMP} "patch")
local V=""
# When patch bump then always use the prepared current version without tag
# 1.0.1-SNAPSHOT and patch then 1.0.1
if [ "${BUMP}" == "patch" ]; then
V="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
fi
# When minor bump then use the prepared current version if the patch is 0, if not release a next minor
# 1.1.0-SNAPSHOT and minor then 1.1.0
# 1.1.3-SNAPSHOT and minor then 1.2.0
if [ "${BUMP}" == "minor" ]; then
if [ "${VERSION_PATCH}" == "0" ]; then
V="${VERSION_MAJOR}.${VERSION_MINOR}.0"
else
V="${VERSION_BUMP_MAJOR}.${VERSION_BUMP_MINOR}.0"
fi
fi
# When major bump then use the prepared current version if the minor is 0 and patch is 0, if not release a next major
# 1.0.0-SNAPSHOT and major then 1.0.0
# 1.1.3-SNAPSHOT and major then 2.0.0
# 1.2.0-SNAPSHOT and major then 2.0.0
if [ "${BUMP}" == "major" ]; then
if [ "${VERSION_MINOR}" == "0" ] && [ "${VERSION_PATCH}" == "0" ]; then
V="${VERSION_MAJOR}.0.0"
else
V="${VERSION_BUMP_MAJOR}.0.0"
fi
fi
echo "${V}"
elif [ "${BUMP}" == "date" ]; then
echo "${VERSION}"
fi
}
function next_version() {
local VERSION="$1"
local BUMP="$2"
local TAG="$3"
if [ "${BUMP}" == "major" ] || [ "${BUMP}" == "minor" ] || [ "${BUMP}" == "patch" ]; then
local VERSION_MAJOR=$(parse_version ${VERSION} "major")
local VERSION_MINOR=$(parse_version ${VERSION} "minor")
local VERSION_PATCH=$(parse_version ${VERSION} "patch")
local VERSION_TAG=$(parse_version ${VERSION} "tag")
if [ "${BUMP}" == "major" ]; then
VERSION_MAJOR=$((${VERSION_MAJOR} + 1))
VERSION_MINOR="0"
VERSION_PATCH="0"
fi
if [ "${BUMP}" == "minor" ]; then
VERSION_MINOR=$((${VERSION_MINOR} + 1))
VERSION_PATCH="0"
fi
if [ "${BUMP}" == "patch" ]; then
VERSION_PATCH=$((${VERSION_PATCH} + 1))
fi
echo "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}${TAG}"
elif [ "${BUMP}" == "date" ]; then
echo "${VERSION}"
fi
}
function checks() {
local CONTEXT_DIRECTORY="$1"
local BUILDER="$2"
local VERSION="$3"
if [ -z "${BUILDER}" ]; then
echo "No builder provided"
exit 1
fi
if [ "${BUILDER}" == "gradle" ] && [ ! -f "${CONTEXT_DIRECTORY}/gradle.properties" ]; then
echo "Gradle builder but not file gradle.properties"
exit 1
fi
if [ "${BUILDER}" == "maven" ] && [ ! -f "${CONTEXT_DIRECTORY}/pom.xml" ]; then
echo "Maven builder but not file pom.xml"
exit 1
fi
if [ -n "$(git status --porcelain)" ]; then
echo "Not clean git workspace"
git status
exit 1
fi
if [ -n "$(git ls-remote --tags origin ${VERSION_PREFIX}${VERSION})" ]; then
echo "Release git tag exists"
exit 1
fi
}
function release_git_add() {
local BUILDER="$1"
if [ "${BUILDER}" == "gradle" ]; then
find . -name "gradle.properties" | xargs git add
fi
if [ "${BUILDER}" == "maven" ]; then
find . -name "pom.xml" | xargs git add
fi
if [ "${BUILDER}" == "npm" ]; then
find . -maxdepth 1 -name "package*.json" | xargs git add
fi
}
function pre_release() {
local CONTEXT_DIRECTORY="$1"
local BUILDER="$2"
local RELEASE_VERSION="$3"
local RELEASE_TAG="$4"
set_version "${CONTEXT_DIRECTORY}" "${BUILDER}" "${RELEASE_VERSION}"
release_git_add "${BUILDER}"
git commit -m "[ci skip] [gha-release] Release ${RELEASE_VERSION}"
git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_VERSION}"
git push origin "${RELEASE_TAG}"
git push origin HEAD
}
function post_release() {
local CONTEXT_DIRECTORY="$1"
local BUILDER="$2"
local VERSION="$3"
set_version "${CONTEXT_DIRECTORY}" "${BUILDER}" "${VERSION}"
release_git_add "${BUILDER}"
git commit -m "[ci skip] [gha-release] Prepare ${VERSION}"
git push origin HEAD
}
function information() {
local SNAPSHOT_TAG="$1"
local BRANCH="$2"
local IS_RELEASE_BRANCH="$3"
local CURRENT_VERSION="$4"
local RELEASE_VERSION="$5"
local RELEASE_TAG="$6"
local NEXT_VERSION="$7"
local VERSION="$8"
local TAG="$9"
JSON_STRING=$(jq -n \
--arg SNAPSHOT_TAG "${SNAPSHOT_TAG}" \
--arg BRANCH "${BRANCH}" \
--arg IS_RELEASE_BRANCH "${IS_RELEASE_BRANCH}" \
--arg CURRENT_VERSION "${CURRENT_VERSION}" \
--arg RELEASE_VERSION "${RELEASE_VERSION}" \
--arg RELEASE_TAG "${RELEASE_TAG}" \
--arg NEXT_VERSION "${NEXT_VERSION}" \
--arg VERSION "${VERSION}" \
--arg TAG "${TAG}" \
'{"snapshot-tag": $SNAPSHOT_TAG, "branch": $BRANCH, "is-release-branch": $IS_RELEASE_BRANCH, "current-version": $CURRENT_VERSION, "release-version": $RELEASE_VERSION, "release-tag": $RELEASE_TAG, "next-version": $NEXT_VERSION, "version": $VERSION, "tag": $TAG}')
echo "${JSON_STRING}"
}
function perform() {
local CONTEXT_DIRECTORY="$1"
local BUILDER="$2"
local RELEASE_VERSION="$3"
local RELEASE_TAG="$4"
local NEXT_VERSION="$5"
checks "${CONTEXT_DIRECTORY}" "${BUILDER}" "${RELEASE_VERSION}"
if [ "${DRY_RUN}" == "false" ]; then
pre_release "${CONTEXT_DIRECTORY}" "${BUILDER}" "${RELEASE_VERSION}" "${RELEASE_TAG}"
if [ -n "${NEXT_VERSION}" ]; then
post_release "${CONTEXT_DIRECTORY}" "${BUILDER}" "${NEXT_VERSION}"
fi
fi
}
function main() {
local CONTEXT_DIRECTORY="."
local BUILDER=""
local BUMP=""
local BUMP_DEFAULT=""
local TASK_FILE=""
local INFORMATION_OPTION="false"
local PERFORM_OPTION="false"
local VERBOSE_OPTION="false"
while getopts "c:b:u:w:t:dip" arg; do
case $arg in
c)
CONTEXT_DIRECTORY="${OPTARG}"
;;
b)
BUILDER="${OPTARG}"
;;
u)
BUMP="${OPTARG}"
;;
w)
BUMP_DEFAULT="${OPTARG}"
;;
t)
TASK_FILE="${OPTARG}"
;;
d)
DRY_RUN="true"
;;
i)
INFORMATION_OPTION="true"
;;
p)
PERFORM_OPTION="true"
;;
v)
VERBOSE_OPTION="true"
;;
*) ;;
esac
done
if [ "${VERBOSE_OPTION}" == "true" ]; then
set -o xtrace
fi
local SNAPSHOT_TAG=$(get_snapshot_tag ${BUILDER})
local BRANCH="$(git branch --show-current)"
local IS_RELEASE_BRANCH="false"
local CURRENT_VERSION=$(get_version ${TASK_FILE} ${BUILDER})
local RELEASE_VERSION=""
if [[ "${CURRENT_VERSION}" != *"-SNAPSHOT" ]]; then
RELEASE_VERSION=$(next_version "${CURRENT_VERSION}" "${BUMP}" "")
else
RELEASE_VERSION=$(release_version "${CURRENT_VERSION}" "${BUMP}")
fi
local RELEASE_TAG="v${RELEASE_VERSION}"
local NEXT_VERSION=""
if [[ "${CURRENT_VERSION}" == *"-SNAPSHOT" ]]; then
NEXT_VERSION=$(next_version "${RELEASE_VERSION}" "${BUMP_DEFAULT}" "${SNAPSHOT_TAG}")
fi
local VERSION=""
local TAG=""
if [ "${BRANCH}" == "main" ] || [ "${BRANCH}" == "master" ]; then
VERSION="${RELEASE_VERSION}"
IS_RELEASE_BRANCH="true"
else
VERSION=$(echo "${CURRENT_VERSION}" | sed -e "s|${SNAPSHOT_TAG}|-${BRANCH//\//-}${SNAPSHOT_TAG}|g")
IS_RELEASE_BRANCH="false"
fi
TAG="v${VERSION}"
if [ "${INFORMATION_OPTION}" == "true" ]; then
information "${SNAPSHOT_TAG}" "${BRANCH}" "${IS_RELEASE_BRANCH}" "${CURRENT_VERSION}" "${RELEASE_VERSION}" "${RELEASE_TAG}" "${NEXT_VERSION}" "${VERSION}" "${TAG}"
fi
if [ "${PERFORM_OPTION}" == "true" ]; then
perform "${CONTEXT_DIRECTORY}" "${BUILDER}" "${RELEASE_VERSION}" "${RELEASE_TAG}" "${NEXT_VERSION}"
fi
}
main "$@"
|
.github/actions/build-release-git/git-release.sh
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
|
name: deploy-gke
description: 'Deploy GKE'
inputs:
dry-run:
type: boolean
required: false
default: true
gar-host:
required: true
type: string
gar-project:
required: true
type: string
gar-docker-repository:
required: true
type: string
project-id:
required: true
type: string
deployment-directory:
required: true
type: string
container-image-name:
required: true
type: string
version:
required: true
type: string
runs:
using: 'composite'
steps:
- name: Setup GKE credentials
uses: ./.github/actions/setup-gke-credentials
if: ${{ !(inputs.dry-run == 'true' || inputs.dry-run == true) }}
with:
project-id: ${{ inputs.project-id }}
- name: Setup Kustomize
uses: ./.github/actions/setup-kustomize
if: ${{ !(inputs.dry-run == 'true' || inputs.dry-run == true) }}
- name: Deploy to GKE
uses: ./.github/actions/deploy-gke-kustomize
if: ${{ !(inputs.dry-run == 'true' || inputs.dry-run == true) }}
with:
deployment-directory: ${{ inputs.deployment-directory }}
image: ${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:latest=${{ inputs.gar-host }}/${{ inputs.gar-project }}/${{ inputs.gar-docker-repository }}/${{ inputs.container-image-name }}:${{ inputs.version }}
|
.github/actions/deploy-gke/action.yml
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
|
name: deploy-notification-slack
description: 'Deploy notification Slack'
inputs:
dry-run:
type: string
required: false
ref:
type: string
required: false
slack-webhook-url:
type: string
required: true
job-steps:
type: string
required: true
environment:
type: string
required: true
runtime:
type: string
required: true
gcp-project:
type: string
required: true
gar-npm-repository-location:
type: string
required: false
gar-npm-repository:
type: string
required: false
issue-id:
type: string
required: true
issue-summary:
type: string
required: true
repository:
type: string
required: true
pull-request-number:
type: string
required: true
pull-request-branch:
type: string
required: true
version:
type: string
required: true
tag:
type: string
required: true
commit-hash:
type: string
required: true
branch:
type: string
required: true
build-number:
type: string
build-date:
type: string
runs:
using: 'composite'
steps:
- name: Get information
id: info
shell: bash
run: |-
STATUS_EMOJI=":large_yellow_circle:"
STATUS_LABEL=" (status?)"
if [ "${{ job.status }}" == "failure" ]; then
JOB_STEPS=$(cat << EOF
${{ inputs.job-steps }}
EOF
)
echo "Steps: ${JOB_STEPS}"
STEP_FAILURE=$(echo "${JOB_STEPS}" | jq --raw-output 'to_entries[] | select(.value.outcome == "failure").key')
echo "Steps failure: ${STEP_FAILURE}"
STATUS_EMOJI=":red_circle:"
STATUS_LABEL=" (${STEP_FAILURE})"
elif [ "${{ job.status }}" == "cancelled" ]; then
STATUS_EMOJI=":red_circle:"
STATUS_LABEL=" (cancelled)"
elif [ "${{ inputs.dry-run }}" == "true" ]; then
STATUS_EMOJI=":large_purple_circle:"
STATUS_LABEL=" (dry-run)"
elif [ "${{ job.status }}" == "success" ]; then
STATUS_EMOJI=":large_green_circle:"
STATUS_LABEL=""
fi
if [ "${{ inputs.runtime }}" == "gke" ]; then
GCP_PROJECT_URL="https://console.cloud.google.com/kubernetes/workload/overview?project=${{ inputs.gcp-project }}"
elif [ "${{ inputs.runtime }}" == "gae" ]; then
GCP_PROJECT_URL="https://console.cloud.google.com/appengine/services?project=${{ inputs.gcp-project }}"
elif [ "${{ inputs.runtime }}" == "npm-repository" ]; then
GCP_PROJECT_URL="https://console.cloud.google.com/artifacts/npm/${{ inputs.gcp-project }}/${{ inputs.gar-npm-repository-location }}/${{ inputs.gar-npm-repository }}"
else
GCP_PROJECT_URL="https://console.cloud.google.com/welcome?project=${{ inputs.gcp-project }}"
fi
BRANCH="<https://github.com/${{ github.repository }}/tree/${{ inputs.branch || inputs.ref }}|${{ inputs.branch || inputs.ref }}>"
if [ -n "${{ inputs.pull-request-branch }}" ] && [ "${{ inputs.pull-request-branch }}" != "${{ inputs.branch }}" ]; then
BRANCH="<https://github.com/${{ github.repository }}/tree/${{ inputs.branch || inputs.ref }}|${{ inputs.branch || inputs.ref }}> from <https://github.com/${{ github.repository }}/tree/${{ inputs.pull-request-branch }}|${{ inputs.pull-request-branch }}>"
fi
RUN_ATTEMPT=""
if [ "${{ github.run_attempt }}" != "1" ]; then
RUN_ATTEMPT=" (${{ github.run_attempt }})"
fi
ACTOR="<https://github.com/${{ github.actor }}|${{ github.actor }}>"
if [ -n "${{ github.triggering_actor }}" ] && [ "${{ github.actor }}" != "${{ github.triggering_actor }}" ]; then
ACTOR="<https://github.com/${{ github.triggering_actor }}|${{ github.triggering_actor }}> on behalf of <https://github.com/${{ github.actor }}|${{ github.actor }}>"
fi
echo "status-emoji=${STATUS_EMOJI}" >> $GITHUB_OUTPUT
echo "status-label=${STATUS_LABEL}" >> $GITHUB_OUTPUT
echo "gcp-project-url=${GCP_PROJECT_URL}" >> $GITHUB_OUTPUT
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "run-attempt=${RUN_ATTEMPT}" >> $GITHUB_OUTPUT
echo "actor=${ACTOR}" >> $GITHUB_OUTPUT
- name: Send job notification to Slack
id: slack
uses: slackapi/slack-github-action@v1.25.0
env:
SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.info.outputs.status-emoji }}${{ steps.info.outputs.status-label }} Deployed *${{ inputs.repository }}* in *${{ inputs.environment }}* by *${{ github.actor }}*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":jira: Issue: <https://acmehub.atlassian.net/browse/${{ inputs.issue-id || 'XXX-0000' }}|${{ inputs.issue-id || 'XXX-0000' }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":jira: Summary: ${{ inputs.issue-summary || '' }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":github: Repository: <https://github.com/${{ github.repository }}|${{ inputs.repository }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":github: Pull request: <https://github.com/${{ github.repository }}/pull/${{ inputs.pull-request-number || 0 }}|${{ inputs.pull-request-number || 0 }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":hammer_and_wrench: Environment: ${{ inputs.environment }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":gear: Runtime: ${{ inputs.runtime }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":gcp: GCP project: <${{ steps.info.outputs.gcp-project-url }}|${{ inputs.gcp-project }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":git: Branch: ${{ steps.info.outputs.branch }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":git: Commit: <https://github.com/${{ github.repository }}/commit/${{ inputs.commit-hash }}|${{ inputs.commit-hash }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":package: Version: ${{ inputs.version }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":dart: Build: <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.run_number }}>${{ steps.info.outputs.run-attempt }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":female-technologist: Actor: ${{ steps.info.outputs.actor }}"
}
}
]
}
|
.github/actions/deploy-notification-slack/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
name: setup-gradle
description: 'Setup Gradle'
runs:
using: 'composite'
steps:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Configure Task
shell: bash
run: |-
echo "GRADLE_OPTIONS=-console plain" >> .env
|
.github/actions/setup-gradle/action.yml
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
|
name: setup-java
description: 'Setup Java'
inputs:
java-version:
type: string
required: false
default: '21'
java-distribution:
type: string
required: false
default: 'temurin'
cache:
type: string
required: false
runs:
using: 'composite'
steps:
- name: Setup JDK ${{ inputs.java-version }}-${{ inputs.java-distribution }}
uses: actions/setup-java@v4
if: ${{ inputs.cache == null || inputs.cache == '' }}
with:
java-version: ${{ inputs.java-version }}
distribution: ${{ inputs.java-distribution }}
cache: ${{ inputs.cache }}
|
.github/actions/setup-java/action.yml
Este es el aspecto de la notificación en Slack.
Cambios a futuro
Los pipelines probablemente son trajes a medida según la organización cada una es diferente aunque todas comparten muchas necesidades. Este ejemplo tampoco es completo, por ejemplo podría añadirse la parte de pruebas una vez desplegado el artefacto en un entorno, pruebas funcionales, de rendimiento, seguridad, etc.
Como ideas para futuro tengo el integrar en los workflows las labels que se pueden asociar a los pull request para modificar de alguna forma útil el comportamiento. Activar Dependabot sin que sea demasiado pelmazo con las alertas. E incluso como en realidad los workflows en los repositorios son una colección de propiedades podría mover esa configuración dentro del repositorio de los workflows, aunque tiene la desventaja que al desarrollador tendría que hacer commits en diferentes repositorios, asi que igual no es buena idea.
En realidad creo que estoy en una quest de sacar el máximo partido de Github, tiene otras opciones interesantes como Code scanning, Secret scanning y Github Copilot. La otra quest es intentar reemplazar Jenkins y Nexus para la que ya tengo unas ideas que explorar, y esto sería una enorme mejora que permitiría reemplazar una infraestructura muy antigua con su consiguiente ahorro en costes y mantenimiento.
Es interesante también archivar los informes generados en caso de que un paso de check falle en la integración continua. O crear algún workflow periódico que no es útil ejecutar en cada push a una rama como comprobaciones de seguridad en proyectos Java con Owasp para comprobar las dependencias o la cobertura de los teses unitarios.
Si me es posible a medida que realice estos cambios actualizaré el artículo.