Construyendo un pipeline de CI/CD con Github Actions

Escrito por el , actualizado el .
programacion
Enlace permanente Comentarios

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.

GitHub

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.

Workflow de Github Actions Notificación de deploy en Slack

Workflow de Github Actions y notificación de deploy 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.


Comparte el artículo: