Integración continua con acciones de GitHub en SwiftUI
¡Una vez lanzadas las acciones de Github, permite que tengamos una oportunidad para habilitar fácilmente la integración continua en nuestros proyectos en GitHub, así que vamos a ello. Este artículo cubre un enfoque simple para realizar CI para proyectos de iOS, utilizando las acciones de Github. Esta nueva herramienta nos otorga una opción más rápida para una CI efectiva en nuestros proyectos de GitHub. Así que sin más preámbulos, vamos a configurarlo, primero crearemos nuestro proyecto en SwiftUI con los Test por defecto.
Como parte de nuestra correcta escritura de nuestro código, lo que hacemos normalmente es la validación con swiftlint, ya sabemos que podemos instalar dicha herramienta con homebrew, pero vamos a conocer otra herramienta que nos ayuda con eso y seguro sera nueva para alguno, Mint, es ideal para la mayoría de los paquetes CLI que normalmente descargaríamos con Homebrew, ya que puede descargar y ejecutar fácilmente una versión específica de un paquete, pero debemos descargarnos primero mint con la siguente instrucción en nuestras terminal:
brew install mint
De esta manera, podemos estar seguros de que estamos ejecutando la versión correcta en Acciones de GitHub. Solo hay que crearnos un archivo en la raíz del proyecto llamadado Mintfile, y dentro metemos la siguiente línea de código: realm/SwiftLint@0.42.0, bien!! más adelante crearemos un script para instalar Mint desde Github Actions.
Preparando las Gemas de Ruby
Antes que nada hay que tener encuenta la retrocompatibilidad de los sistemas a día de hoy mi MacBookPro tiene Catalina con la version 10.15 y ruby entra en conflictos y como vamos a conocer estas herramientas os recomiendo antes realizar unas cuantas actualizaciones:
- rvm install ruby@latest (última version de ruby -> 3)
Debes tener rbenv instalado, pero debes tener cuidado ya que vas a ejecutar un script de ruby en el modo mas privado no introduzcas la palabra sudo:
- brew install rbenv
Usaremos Bundler, por lo que tenemos un Gemfile que especifica las dependencias de RubyGem que necesitamos para hacer CI. Si aún no tienes Bundler instalado en tu Mac, ejecuta en tu terminal:
- sudo gem install bundler:2.2.9 (compatible con la ultima versión de ruby)
Busca el directorio de tu proyecto a través del terminal y ejecuta:
- bundle init (Esto se parece mucho a Cocoapods o fastlane no ??)
Esta instrucción creará un Gemfile. Aquí están las dependencias que necesitamos instalar, así que introduce el siguiente código en tu Gemfile recién creado (Yeah!!):
source “https://rubygems.org"
gem “danger”
gem “danger-swiftlint”
gem “fastlane”
gem “xcode-install”
Ahora que hemos especificado las dependencias que necesitamos, instalalas ejecutando:
- bundle install
Esto también agrega un Gemfile.lock a tu proyecto, especificando las versiones de las dependencias que se instalaron.
Script de automatización
Vamos a automatizar la instalación de dependencias de nuestro proyecto, esto nos ayuda a eliminar el error humano, y mejorar nuestro tiempo de despliegue y mejoras de nuestra App, para ello vamos a escribir el siguiente script y le llamaremos bootstrap.sh, este debe estar ubicado en la raíz de nuestro proyecto:
#!/bin/sh
echo “checking for homebrew updates”;
brew update
function install_current {
echo “trying to update $1”
brew upgrade $1 || brew install $1 || true
brew link $1
}
if [ -e “Mintfile” ]; then
install_current mint
mint bootstrap
fi
# Install gems if a Gemfile exists
if [ -e “Gemfile” ]; then
echo “installing ruby gems”;
# install bundler gem for ruby dependency management
gem install bundler — no-document || echo “failed to install bundle”;
bundle config set deployment ‘true’;
bundle config path vendor/bundle;
bundle install || echo “failed to install bundle”;
fi
Ok, vamos bien!, el ejecutar el sh boostrap.sh desde nuestra terminal, tendremos todas las dependendencias listas y simplicando mucho de configuración para GitHub.
Creación del flujo de trabajo
Ahora, necesitamos crear el flujo de trabajo que se activará al crear una solicitud de extracción hacia la rama principal de nuestro repositorio.
- Vamos al repo en GitHub y le damos al boton “Actions”
2. Entramos a esa opcion “Set up a workflow yourself” y una vez allí, modificamos el nombre de main.yml por ci.yml
3. Copia y pega el siguente Pipe en el editor
name: CI
on:
pull_request:
branches:
- main
jobs:
validate-and-test:
runs-on: macOS-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Cache RubyGems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: ${{ runner.os }}-gem-
- name: Cache Mint packages
uses: actions/cache@v1
with:
path: ${{ env.MINT_PATH }}
key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
restore-keys: ${{ runner.os }}-mint-
- name: Install dependencies
run: sh ./bootstrap.sh
- name: Run code validation
run: bundle exec danger
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
- name: Run tests
run: bundle exec fastlane test
env:
MINT_PATH: ${{ github.workspace }}/mint
Lo que debe extraer de esto es que tenemos un job llamado validate-and-test. Consta de varios pasos que son tareas sencillas que se ejecutan una tras otra. Si pasa un paso, pasamos al siguiente. En esta configuración, tenemos los siguientes pasos:
- Obtener dependencias almacenadas en caché, si las hay. Esto usa una acción oficial llamada “caché”. Puedes leer sobre los conceptos de acciones aquí .
- Instala todas las dependencias usando el boostrap.sh si no se encontró caché.
- Ejecuta la validación de código con Danger. Esto lee el token de acceso proporcionado como una variable de entorno.
- Ejecuta pruebas con Fastlane. El comando bundle exec fastlane test activa un carril llamado test en nuestro Fastfile que ejecuta las pruebas.
Si no estás familiarizado con los conceptos de flujos de trabajo, pasos y secuencias, te recomiendo que lea sobre la sintaxis del flujo de trabajo en los documentos de Acciones de GitHub.
Validación de código usando Danger + SwiftLint
Danger es una de las dependencias que especificamos en nuestro Gemfile anteriormente lo recuerdas ??. Es una gran herramienta para mejorar nuestras revisiones de código y simplificar las solicitudes de extracción a nuestro proyecto.
Estas tareas pueden ser desde problemas de formato en el código hasta trámites como no dejar una descripción en la solicitud de extracción. Tú decides las reglas usando el Dangerfile. Lo que sucede es que Danger dejará mensajes en sus solicitudes de extracción cuando infrinja alguna de las reglas, creamos el fichero Dangerfile en la raiz de nuestro proyecto con las siguiente configuracion:
#### HELPER METHODS
def lineContainsPublicPropertyMethodClassOrStruct(line)
if lineIsPropertyMethodClassOrStruct(line) and line.include?("public")
return true
end
return false
end
def lineIsPropertyMethodClassOrStruct(line)
if line.include?("var") or line.include?("let") or line.include?("func") or line.include?("class") or line.include?("struct")
return true
end
return false
end
# Make it more obvious that a PR is a work in progress and shouldn't be merged yet.
has_wip_label = github.pr_labels.any? { |label| label.include? "WIP" }
has_wip_title = github.pr_title.include? "[WIP]"
if has_wip_label || has_wip_title
warn("PR is classed as Work in Progress")
end
# Warn when there is a big PR.
warn("Big PR") if git.lines_of_code > 500
# Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title.
if github.pr_body.length < 3 && git.lines_of_code > 10
warn("Please provide a summary in the Pull Request description")
end
#
# ** FILE CHECKS **
# Checks for certain rules and warns if needed.
# Some rules can be disabled by using // danger:disable rule_name
#
# Rules:
# - Check to see if any of the modified or added files contains a class which isn't indicated as final (final_class)
# - Check for large files without any // MARK:
# - Check for the usage of unowned self. We rather like to use weak self to be safe.
# - Check for override methods which only implement super calls. These can be removed.
# - Check for public properties or methods which aren't documented (public_docs)
# Sometimes an added file is also counted as modified. We want the files to be checked only once.
files_to_check = (git.modified_files + git.added_files).uniq
(files_to_check - %w(Dangerfile)).each do |file|
next unless File.file?(file)
# Only check for classes inside swift files
next unless File.extname(file).include?(".swift")
# Will be used to check if we're inside a comment block.
isCommentBlock = false
# Will be used to track if we've placed any marks inside our class.
foundMark = false
# Collects all disabled rules for this file.
disabled_rules = []
filelines = File.readlines(file)
filelines.each_with_index do |line, index|
if isCommentBlock
if line.include?("*/")
isCommentBlock = false
end
elsif line.include?("/*")
isCommentBlock = true
elsif line.include?("danger:disable")
rule_to_disable = line.split.last
disabled_rules.push(rule_to_disable)
else
# Start our custom line checks
## Check for the usage of final class
if disabled_rules.include?("final_class") == false and line.include?("class") and not line.include?("final") and not line.include?("func") and not line.include?("//") and not line.include?("protocol")
warn("Consider using final for this class or use a struct (final_class)", file: file, line: index+1)
end
## Check for the usage of unowned self
if line.include?("unowned self")
warn("It's safer to use weak instead of unowned", file: file, line: index+1)
end
## Check for methods that only call the super class' method
if line.include?("override") and line.include?("func") and filelines[index+1].include?("super") and filelines[index+2].include?("}")
warn("Override methods which only call super can be removed", file: file, line: index+3)
end
## Check if our line includes a MARK:
if line.include?("MARK:") and line.include?("//")
foundMark = true
end
end
end
## Check wether our file is larger than 200 lines and doesn't include any Marks
if filelines.count > 200 and foundMark == false
warn("Consider to place some `MARK:` lines for files over 200 lines big.")
end
end
# This is a swiftlint plugin. More info: https://github.com/ashfurrow/danger-swiftlint
swiftlint.config_file = '.swiftlint.yml'
swiftlint.lint_files inline_mode: true
Necesitamos proporcionar a Danger un token de acceso. Vaya al token de acceso personal de GitHub en Configuración y genere un nuevo token de acceso. Puede llamarlo “danger” si te parece bien. Luego, debes ir a la sección de secretos en Configuración en su proyecto de GitHub. Copia / pega el token de acceso a un nuevo secreto llamado “DANGER_GITHUB_API_TOKEN”. Eso le dará a Danger el acceso que necesita.
Recuerdas que también especificamos danger-swiftlint en el Gemfile. Es un complemento para Danger que nos permite usar SwiftLint en combinación con Danger para realizar el linting e informar si hay alguna infracción. Cree un archivo “.swiftlint.yml” en la raíz de su proyecto. Puedes copiar / pegar las reglas de swiftlint que normalmente uso:
disabled_rules: # rule identifiers to exclude from running
- line_length
- function_body_length
- trailing_whitespace
- vertical_whitespace
- file_length
- trailing_newline
- force_cast
- large_tuple
- identifier_name
- multiple_closures_with_trailing_closure
- legacy_constructor
- type_name
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
- FinniversKit
- "*/*/*.generated.swift" # dependent on file hierarchy, no fix as of yet
Por último, asegurate de tener estas dos líneas en su Dangerfile, al final del fichero:
- swiftlint.config_file = ‘.swiftlint.yml’
- swiftlint.lint_files inline_mode: true
¡Esa es la validación del código! Ahora, en las pruebas automatizadas, por fin (Yeah!!!).
Pruebas con el escaneo Fastlane
Primero debemos comprobar en nuestra máquina si tenemos fastlane instalado, de no estar instalada ejecutamos el siguiente comando:
- sudo gem install fastlane
Ejecutamos la inicialización de fastlane en la raíz de nuestro proyecto:
- fastlane init
Dentro de crean dos ficheros Appfile / Fastfile, estos los podemos personaizar:
Fastfile -> ten en cuenta el nombre de tu Schema en Xcode
default_platform(:ios)
before_all do
xcversion(version: "11.3")
end
platform :ios do
desc 'Runs the tests in Introspect'
lane :test do
scan(scheme: "CIGitHubActions")
end
end
Creamos una tercer fichero Scanfile con la instrucción fastlane scan init y lo personalizamos con la siguiente info:
Scanfile ->
devices(["iPhone 11 Pro"])
reset_simulator(true)
clean(true)
Ahora, ejecutamos fastlane scan initfastlane scan init
su proyecto para crear un archivo de escaneo dentro de la carpeta fastlane. Este es un archivo de configuración con todos los parámetros predeterminados que queremos establecer cuando ejecutamos las pruebas.
Bien!! ya está, ahora en tu IDE de git (Kraken / Sourcetree ), a partir de main creamos una nueva feature/PR01 por ejemplo y cambia el Hello, World del ContentView.swift y commitea en esa rama, una vez allí te vas a GitHub y abres un PullRequest, automáticamente se activa nuestro CI personalizado y ejecutas el job y Voilà!!, ya podemos ver GitHub realizando un trabajo de sencillamente espectacular!!