article illustration Automatischer Capacitor iOS-Build mit GitHub Actions unter Verwendung von match
CI/CD
Last update: August 01, 2024

Automatischer Capacitor iOS-Build mit GitHub Actions unter Verwendung von match

So richten Sie eine CI/CD-Pipeline für Ihre iOS Ionic-App mit fastlane und GitHub Actions in 5 Minuten ein (2022)

Kontinuierliche Auslieferung für iOS mit Fastlane und GitHub Actions unter Verwendung von match

Voraussetzungen

Bevor Sie mit dem Tutorial fortfahren…

  • Stellen Sie sicher, dass Sie Fastlane auf Ihrem Entwicklungsrechner installiert haben
  • iOS-Entwicklerprogramm-Mitgliedschaft
  • Lust zum Lesen 😆…
  • Ein Team aus vielen Entwicklern, ansonsten empfehlen wir die Verwendung von fastlane cert für einfachere Workflows

Wichtiges zum Preis

Preis GitHub Action

https://github.com/features/actions

Der Service ist bis zum Limit ‘kostenlos’, abhängig von der gewählten Maschine
Wir werden eine macOS-Maschine verwenden, Sie können im Screenshot den Preis und die Limits sehen (Preise zum Zeitpunkt der Erstellung des Tutorials, sie könnten sich in Zukunft ändern)

🔴 Nachdem wir über Anforderungen und Preise gewarnt haben, fahren wir fort, wenn Sie möchten…

📣 In diesem Beitrag gehen wir davon aus, dass wir die App in iTunes Connect erstellt haben, wir haben die Zertifikate des Apple-Ökosystems, alles wird von Fastlane kopiert!

Lass uns loslegen 🧑🏽‍💻

Schritte, die im Beitrag zu befolgen sind

  1. Verwendung der App Store Connect API mit Fastlane Match
  2. Anforderungen
  3. Erstellung eines App Store Connect API-Schlüssels
  4. Verwendung eines App Store Connect API-Schlüssels
  5. Kopieren der Fastlane-Dateien
  6. Konfiguration von Fastlane match

1. Verwendung der App Store Connect API mit Fastlane Match

Ab Februar 2021 ist die Zwei-Faktor-Authentifizierung oder Zwei-Schritt-Verifizierung für alle Benutzer erforderlich, um sich bei App Store Connect anzumelden. Diese zusätzliche Sicherheitsebene für Ihre Apple-ID hilft sicherzustellen, dass nur Sie auf Ihr Konto zugreifen können.
Von Apple Support

Der Einstieg in match erfordert, dass Sie Ihre bestehenden Zertifikate widerrufen. Aber keine Sorge, Sie erhalten direkt das neue

Anforderungen

Um die App Store Connect API nutzen zu können, benötigt Fastlane drei Dinge:

  1. Aussteller-ID
  2. Schlüssel-ID
  3. Schlüsseldatei oder Schlüsselinhalt

Erstellung eines App Store Connect API-Schlüssels

Um Schlüssel zu generieren, müssen Sie über Administratorberechtigungen in App Store Connect verfügen. Wenn Sie diese Berechtigung nicht haben, können Sie die relevante Person auf diesen Artikel verweisen und die folgenden Anweisungen befolgen:

1 — Melden Sie sich bei App Store Connect an

2 — Wählen Sie Benutzer und Zugriff

App Store Connect Benutzerzugriff

3 — Wählen Sie die Registerkarte API-Schlüssel

App Store Connect API-Schlüssel

4 — Klicken Sie auf API-Schlüssel generieren oder auf die Schaltfläche Hinzufügen (+)

App Store Connect API-Schlüssel erstellen

5 — Geben Sie einen Namen für den Schlüssel ein. Der Name dient nur zu Ihrer Information und ist nicht Teil des Schlüssels selbst

App Store Connect API-Schlüssel Namen erstellen

6 — Wählen Sie unter Zugriff die Rolle für den Schlüssel aus. Die Rollen, die für Schlüssel gelten, sind dieselben Rollen, die für Benutzer in Ihrem Team gelten. Siehe Rollenberechtigungen

7 — Klicken Sie auf Generieren

Der Zugriff eines API-Schlüssels kann nicht auf bestimmte Apps beschränkt werden

Der Name des neuen Schlüssels, die Schlüssel-ID, ein Download-Link und andere Informationen erscheinen auf der Seite

App Store Connect Schlüssel herunterladen

Hier können Sie alle drei notwendigen Informationen erfassen:
· Aussteller-ID
· Schlüssel-ID
· Klicken Sie auf “API-Schlüssel herunterladen”, um Ihren privaten API-Schlüssel herunterzuladen. Der Download-Link erscheint nur, wenn der private Schlüssel noch nicht heruntergeladen wurde. Apple bewahrt keine Kopie des privaten Schlüssels auf. Sie können ihn also nur einmal herunterladen.

🔴 Bewahren Sie Ihren privaten Schlüssel an einem sicheren Ort auf. Sie sollten Ihre Schlüssel niemals teilen, Schlüssel in einem Code-Repository speichern oder Schlüssel in Client-seitigen Code aufnehmen.

Verwendung eines App Store Connect API-Schlüssels

Die API-Schlüsseldatei (p8-Datei, die Sie herunterladen), die Schlüssel-ID und die Aussteller-ID werden benötigt, um den JWT-Token für die Autorisierung zu erstellen.Es gibt mehrere Möglichkeiten, diese Informationen in Fastlane mithilfe der neuen Aktion app_store_connect_api_key einzugeben. Weitere Möglichkeiten finden Sie in der Fastlane-Dokumentation. Ich zeige diese Methode, da ich denke, dass es die einfachste Möglichkeit ist, mit den meisten CI-Systemen zu arbeiten, bei denen Sie Umgebungsvariablen festlegen können.

Jetzt können wir Fastlane mit dem App Store Connect API-Schlüssel verwalten, großartig!

2. Fastlane-Dateien kopieren

Fastlane ist eine Ruby-Bibliothek, die entwickelt wurde, um häufige Aufgaben der mobilen Entwicklung zu automatisieren. Mit Fastlane können Sie benutzerdefinierte “Lanes” konfigurieren, die eine Reihe von “Aktionen” bündeln, die Aufgaben ausführen, die Sie normalerweise mit Android Studio durchführen würden. Sie können viel mit Fastlane machen, aber für die Zwecke dieses Tutorials werden wir nur eine Handvoll Kernaktionen verwenden.

Erstellen Sie einen Fastlane-Ordner im Hauptverzeichnis Ihres Projekts und kopieren Sie die folgenden Dateien: Fastfile

default_platform(:ios)
DEVELOPER_APP_IDENTIFIER = ENV["DEVELOPER_APP_IDENTIFIER"]
DEVELOPER_APP_ID = ENV["DEVELOPER_APP_ID"]
PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"]
TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"]
TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"]
APPLE_ISSUER_ID = ENV["APPLE_ISSUER_ID"]
APPLE_KEY_ID = ENV["APPLE_KEY_ID"]
APPLE_KEY_CONTENT = ENV["APPLE_KEY_CONTENT"]
GIT_USERNAME = ENV["GIT_USERNAME"]
GIT_TOKEN = ENV["GIT_TOKEN"]
def delete_temp_keychain(name)
delete_keychain(
name: name
) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db")
end
def create_temp_keychain(name, password)
create_keychain(
name: name,
password: password,
unlock: false,
timeout: 0
)
end
def ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
end
platform :ios do
lane :build do
build_app(
configuration: "Release",
workspace: "./ios/App/App.xcworkspace",
scheme: "App",
export_method: "app-store",
export_options: {
provisioningProfiles: {
DEVELOPER_APP_ID => "#{PROVISIONING_PROFILE_SPECIFIER}"
}
}
)
end
lane :refresh_profiles do
match(
type: "development",
force: true)
match(
type: "adhoc",
force: true)
end
desc "Register new device"
lane :register_new_device do |options|
device_name = prompt(text: "Enter the device name: ")
device_udid = prompt(text: "Enter the device UDID: ")
device_hash = {}
device_hash[device_name] = device_udid
register_devices(
devices: device_hash
)
refresh_profiles
end
lane :closed_beta do
keychain_name = TEMP_KEYCHAIN_USER
keychain_password = TEMP_KEYCHAIN_PASSWORD
ensure_temp_keychain(keychain_name, keychain_password)
api_key = app_store_connect_api_key(
key_id: APPLE_KEY_ID,
issuer_id: APPLE_ISSUER_ID,
key_content: APPLE_KEY_CONTENT,
duration: 1200,
in_house: false
)
match(
type: 'appstore',
git_basic_authorization: Base64.strict_encode64("#{GIT_USERNAME}:#{GIT_TOKEN}"),
readonly: true,
keychain_name: keychain_name,
keychain_password: keychain_password,
api_key: api_key
)
gym(
configuration: "Release",
workspace: "./ios/App/App.xcworkspace",
scheme: "App",
export_method: "app-store",
export_options: {
provisioningProfiles: {
DEVELOPER_APP_ID => "#{PROVISIONING_PROFILE_SPECIFIER}"
}
}
)
pilot(
apple_id: "#{DEVELOPER_APP_ID}",
app_identifier: "#{DEVELOPER_APP_IDENTIFIER}",
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
ipa: "./App.ipa"
)
delete_temp_keychain(keychain_name)
end
lane :submit_review do
version = ''
Dir.chdir("..") do
file = File.read("package.json")
data = JSON.parse(file)
version = data["version"]
end
deliver(
app_version: version,
submit_for_review: true,
automatic_release: true,
force: true, # Skip HTMl report verification
skip_metadata: false,
skip_screenshots: false,
skip_binary_upload: true
)
end
end

Appfile

app_identifier(ENV["DEVELOPER_APP_IDENTIFIER"])
apple_id(ENV["FASTLANE_APPLE_ID"])
itc_team_id(ENV["APP_STORE_CONNECT_TEAM_ID"])
team_id(ENV["DEVELOPER_PORTAL_TEAM_ID"])

Fastlane match konfigurieren

Fastlane match ist ein neuer Ansatz für die Code-Signierung von iOS. Fastlane match erleichtert es Teams, die erforderlichen Zertifikate und Bereitstellungsprofile für Ihre iOS-Apps zu verwalten.

Erstellen Sie ein neues privates Repository mit dem Namen certificates, zum Beispiel in Ihrem persönlichen GitHub-Konto oder Ihrer Organisation.

Initialisieren Sie Fastlane match für Ihre iOS-App

Terminal window
fastlane match init

Wählen Sie dann Option #1 (Git Storage)

[01:00:00]: fastlane match supports multiple storage modes, please select the one you want to use:1. git2. google_cloud3. s3?

Weisen Sie die URL des neu erstellten Repositories zu

[01:00:00]: Please create a new, private git repository to store the certificates and profiles there[01:00:00]: URL of the Git Repo: <YOUR_CERTIFICATES_REPO_URL>

Jetzt haben Sie im Fastlane-Ordner eine Datei namens Matchfile, und _git_url_ sollte auf die HTTPS-URL des Zertifikate-Repositories gesetzt sein. Optional können Sie auch SSH verwenden, aber das erfordert einen anderen Schritt zur Ausführung.

# ios/Matchfilegit_url("https://github.com/gitusername/certificates")storage_mode("git")type("appstore")

Als Nächstes generieren wir die Zertifikate und geben Ihre Anmeldedaten ein, wenn Sie mit Fastlane Match dazu aufgefordert werden.

Sie werden aufgefordert, eine Passphrase einzugeben. Merken Sie sie sich gut, da sie später von GitHub Actions verwendet wird, um Ihr Zertifikate-Repository zu entschlüsseln.

fastlane match appstore

Wenn alles gut gelaufen ist, sollten Sie etwas Ähnliches sehen:

[01:40:52]: All required keys, certificates and provisioning profiles are installed 🙌

Wenn Sie Probleme mit GitHub und den erforderlichen Berechtigungen hatten, kann Ihnen dieser Beitrag möglicherweise helfen, Authentifizierungstoken für Git zu generieren.

Generierte Zertifikate und Bereitstellungsprofile werden in die Ressourcen des Zertifikate-Repositories hochgeladen.

App Store Connect Zertifikate

Öffnen Sie zuletzt Ihr Projekt in Xcode und aktualisieren Sie das Bereitstellungsprofil für die Release-Konfiguration Ihrer App.

XCode Zertifikate

Einige Dinge zu beachten 💡

MATCH

Damit CI/CD die Zertifikate und Bereitstellungsprofile importieren kann, muss es Zugriff auf das Zertifikate-Repository haben. Sie können dies tun, indem Sie ein persönliches Zugriffs-Token generieren (sollte zuvor verwendet worden sein), das den Umfang hat, auf private Repositories zuzugreifen oder sie zu lesen.

Gehen Sie in GitHub zu EinstellungenEntwicklereinstellungenPersönliche Zugriffs-Token → klicken Sie auf Neues Token generieren → markieren Sie den repo-Umfang → dann klicken Sie auf Token generieren

Persönliches Zugriffs-Token erstellen

Machen Sie eine Kopie des generierten persönlichen Zugriffs-Tokens. Sie werden es später für die Umgebungsvariable GIT_TOKEN verwenden.

Ersetzen Sie dann Ihre im Fastlane-Ordner generierte Match-Datei durch Matchfile

CERTIFICATE_STORE_URL = ENV["CERTIFICATE_STORE_URL"]
GIT_USERNAME = ENV["GIT_USERNAME"]
GIT_TOKEN = ENV["GIT_TOKEN"]
FASTLANE_APPLE_ID = ENV["FASTLANE_APPLE_ID"]
git_url(CERTIFICATE_STORE_URL)
storage_mode("git")
type("appstore")
git_basic_authorization(Base64.strict_encode64("#{GIT_USERNAME}:#{GIT_TOKEN}"))
username(FASTLANE_APPLE_ID)

Dies wird von GitHub Actions verwendet, um die Zertifikate und Bereitstellungsprofile zu importieren. Und Variablen werden in GitHub Secrets gesetzt, anstatt sie fest in der Datei zu kodieren.

Build-Verarbeitung

Bei GitHub Actions werden Sie basierend auf den Minuten abgerechnet, die Sie für die Ausführung Ihres CI/CD-Workflows verwendet haben. Aus Erfahrung dauert es etwa 10-15 Minuten, bis ein Build in App Store Connect verarbeitet werden kann.

Für private Projekte können die geschätzten Kosten pro Build bis zu $0,08/Min x 15 Min = $1,2 oder mehr betragen, abhängig von der Konfiguration oder den Abhängigkeiten Ihres Projekts.Hier ist die Übersetzung ins Deutsche:

Wenn Sie die gleichen Bedenken bezüglich der Preisgestaltung für private Projekte haben wie ich, können Sie skip_waiting_for_build_processing auf true belassen.

Was ist der Haken? Sie müssen die Compliance Ihrer App in App Store Connect manuell aktualisieren, nachdem der Build verarbeitet wurde, um den Build an Ihre Benutzer verteilen zu können.

Dies ist nur ein optionaler Parameter, den Sie aktualisieren können, wenn Sie bei privaten Projekten Build-Minuten sparen möchten. Für kostenlose Projekte sollte dies überhaupt kein Problem sein. Siehe Preisgestaltung

3. GitHub Actions einrichten

GitHub-Geheimnisse konfigurieren

Haben Sie sich jemals gefragt, woher die Werte der ENV kommen? Nun, es ist kein Geheimnis mehr - sie kommen aus den Geheimnissen Ihres Projekts 🤦

GitHub-Geheimnisse festlegen

  1. APP_STORE_CONNECT_TEAM_ID - die ID Ihres App Store Connect-Teams, wenn Sie in mehreren Teams sind

  2. DEVELOPER_APP_ID - gehen Sie in App Store Connect zur App → App-Informationen → Scrollen Sie zum Abschnitt Allgemeine Informationen Ihrer App und suchen Sie nach Apple ID

  3. DEVELOPER_APP_IDENTIFIER - die Bundle-ID Ihrer App

  4. DEVELOPER_PORTAL_TEAM_ID - die ID Ihres Developer Portal-Teams, wenn Sie in mehreren Teams sind

  5. FASTLANE_APPLE_ID - die Apple-ID oder Entwickler-E-Mail, die Sie zur Verwaltung der App verwenden

  6. GIT_USERNAME & GIT_TOKEN - Ihr Git-Benutzername und Ihr persönlicher Zugriffstoken

  7. MATCH_PASSWORD - die Passphrase, die Sie bei der Initialisierung von Match festgelegt haben, wird zum Entschlüsseln der Zertifikate und Bereitstellungsprofile verwendet

  8. PROVISIONING_PROFILE_SPECIFIER - match AppStore <1>, z.B. match AppStore com.domain.blabla.demo

  9. TEMP_KEYCHAIN_USER & TEMP_KEYCHAIN_PASSWORD - Weisen Sie einen temporären Keychain-Benutzer und ein Passwort für Ihren Workflow zu

  10. APPLE_KEY_ID — App Store Connect API-Schlüssel 🔺Key ID

  11. APPLE_ISSUER_ID — App Store Connect API-Schlüssel 🔺Issuer ID

  12. APPLE_KEY_CONTENT — App Store Connect API-Schlüssel 🔺 Schlüsseldatei oder Schlüsselinhalt von p8, überprüfen Sie es <2>

  13. CERTIFICATE_STORE_URL — Die Repo-URL Ihrer Match-Schlüssel (z.B.: https://github.com/***/fastlane_match.git)

4. GitHub-Workflow-Datei konfigurieren

Erstellen Sie ein GitHub-Workflow-Verzeichnis

cd .github/workflows

Erstellen Sie innerhalb des workflow-Ordners eine Datei namens build-upload-ios.yml und fügen Sie Folgendes hinzu

name: Build source code on ios
on:
push:
tags:
- '*'
jobs:
build_ios:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16
cache: npm
- name: Install dependencies
id: install_code
run: npm ci
- name: Build
id: build_code
run: npm run build
- name: Build
id: build_code
run: npm run mobile
- uses: actions/cache@v3
with:
path: ios/App/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Sync
id: sync_code
run: npx cap sync
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.2
- uses: maierj/fastlane-action@v2.3.0
env:
DEVELOPER_APP_IDENTIFIER: ${{ secrets.DEVELOPER_APP_IDENTIFIER }}
DEVELOPER_APP_ID: ${{ secrets.DEVELOPER_APP_ID }}
PROVISIONING_PROFILE_SPECIFIER: match AppStore ${{ secrets.DEVELOPER_APP_IDENTIFIER }}
TEMP_KEYCHAIN_USER: ${{ secrets.TEMP_KEYCHAIN_USER }}
TEMP_KEYCHAIN_PASSWORD: ${{ secrets.TEMP_KEYCHAIN_PASSWORD }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
CERTIFICATE_STORE_URL: https://github.com/${{ secrets.CERTIFICATE_STORE_REPO }}.git
GIT_USERNAME: ${{ secrets.GIT_USERNAME }}
GIT_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
FASTLANE_APPLE_ID: ${{ secrets.FASTLANE_APPLE_ID }}
MATCH_USERNAME: ${{ secrets.FASTLANE_APPLE_ID }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
DEVELOPER_PORTAL_TEAM_ID: ${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}
with:
lane: closed_beta
- name: Upload release bundle
uses: actions/upload-artifact@v2
with:
name: ios-release
path: ./App.ipa
retention-days: 60

Dieser Workflow sollte nach jedem GitHub-Tag ausgelöst werden. Wenn Sie das Tagging automatisieren möchten, lesen Sie zuerst Automatisches Bauen und Veröffentlichen mit GitHub-Aktionen

Dann wird dieser Workflow Ihre NodeJS-Abhängigkeiten abrufen, sie installieren und Ihre JavaScript-App bauen

Jedes Mal, wenn Sie einen neuen Commit senden, wird eine Release in TestFlight erstellt

Ihre App muss nicht Ionic verwenden, nur die Capacitor-Basis ist erforderlich. Sie kann alte Cordova-Module haben, aber Capacitor JS-Plugins sollten bevorzugt werden

5. Workflow auslösen

Einen Commit erstellen

Machen Sie einen Commit, Sie sollten den aktiven Workflow im Repository sehen

Workflow auslösen

Pushen Sie die neuen Commits in den Branch main oder development, um den Workflow auszulösen

Gestartet mit Commit

Nach einigen Minuten sollte der Build in Ihrem App Store Connect-Dashboard verfügbar sein

Testflight Dashboard

Kann man vom lokalen Rechner aus deployen?

Ja, das können Sie, und es ist ganz einfach

Stellen Sie sich vor, Sie haben ein privates Repository und haben die Minuten des kostenlosen Plans aufgebraucht und möchten nicht für neue Releases bezahlen, oder vielleicht möchten Sie die Anwendung lieber manuell einreichen

Los geht’s

Ok, zuerst müssen wir im Pfad my_project_path/fastlane eine Datei namens env erstellen, genau im gleichen Pfad wie Fastfile, um die gleichen geheimen Eigenschaften wie in unserem GitHub erstellen zu können, wie unten gezeigt:

env-Datei für das Deployment vom lokalen Rechner

Jetzt können Sie zum Terminal gehen und Fastlane von Ihrem Rechner aus starten:

fastlane closed_beta

❌ Wesentlich über das env-Datei, da wir diese Daten lieber nicht offenlegen möchten, müssen wir sie in unserer gitignore hinzufügen, etwa so: ❌

fastlane/*.env

Es sollte genauso funktionieren wie bei GitHub Actions auf der Remote-Maschine, aber auf unserem lokalen Rechner 🍻

Lokaler Fastlane-Lauf

Terminal-Ausführung: $ Fastlane closed_beta

Wenn Sie bis hierher gekommen sind, meine Glückwünsche, jetzt haben Sie einen vollständig automatisierten Prozess für Ihre iOS-Apps mit Fastlane und GitHub Actions

Jedes Mal, wenn Sie einen neuen Commit senden, wird im Google Play Konsole, Beta-Kanal, ein Release erstellt Ich werde diesen Blog mit Ihrem Feedback verbessern. Wenn Sie Fragen oder Vorschläge haben, lassen Sie es mich bitte per E-Mail wissen: martin@capgoapp

Auf Ihrem Gerät bauen

Wenn Sie immer noch auf Ihrem Gerät bauen müssen, müssen Sie sie manuell zur Bereitstellung hinzufügen Verbinden Sie Ihr Gerät mit Ihrem Mac und öffnen Sie das Gerätemenü iOS-Gerätemenü finden Kopieren Sie dann Ihre Kennung iOS-Kennung finden Und starten Sie dann den Befehl: fastlane register_new_device Es wird Sie auffordern, einen Gerätenamen und die Kennung einzugeben: iOS-Kennung setzen

Wenn Sie auf Probleme stoßen

Wenn Sie Probleme mit Entwicklergeräten haben, die nicht testen können usw., behebt das normalerweise das Problem

Es gibt einen magischen Befehl, der Sie retten kann:

Terminal window
fastlane match nuke development
fastlane match development

Dann: Bereinigen Sie das Projekt, indem Sie Umschalt(⇧)+Befehl(⌘)+K gedrückt halten oder Produkt > Bereinigen auswählen (es könnte als “Build-Ordner bereinigen” bezeichnet sein)

Versuchen Sie dann, die App erneut auf Ihrem Gerät auszuführen

Danke

Dieser Blog basiert auf den folgenden Artikeln:

Neueste Nachrichten

Capgo bietet Ihnen die besten Einblicke, die Sie benötigen, um eine wirklich professionelle mobile App zu erstellen.