Slide 1

Slide 1 text

๐Ÿ‘‹ Pol Piella Abadia ๐Ÿ‡ฎ๐Ÿ‡น Swift Heroes 2023 Making developer tools with Swift

Slide 2

Slide 2 text

Who am I? ๐Ÿ‘จ๐Ÿ’ป Senior Software Engineer at BBC โœ Weekly blogs at polpiella.dev ๐Ÿ“ฌ Curator of the iOS CI Newsle tt er ๐Ÿ Based in Manchester ๐Ÿ‡ช๐Ÿ‡ธ From Barcelona

Slide 3

Slide 3 text

What weโ€™ll build today ๐Ÿ’ป Complex metrics system ๐Ÿš€ Metrics from Xcode Cloud and GHA ๐Ÿ“ˆ Dashboard plots and displays metrics ๐Ÿ’ก Inspired by a real-world system ๐Ÿ“ฆ Swift Package monorepo ๐Ÿ†“ Everything is Open Source! โค

Slide 4

Slide 4 text

Why Swift though? ๐Ÿ’ป Often not fi rst choice for writing tooling ๐Ÿš€ Powerful language with a mature ecosystem. โž• Beyond Apple app development ๐Ÿ“ˆ Tools are often maintained and built by mobile developers. โค Focus on the system, not the language!

Slide 5

Slide 5 text

๐Ÿ“ฆ Creating a Swift Package

Slide 6

Slide 6 text

# ! /bin/sh Creating a Swift Package

Slide 7

Slide 7 text

# ! /bin/sh # Create an empty directory mkdir metrics Creating a Swift Package Creating a Swift Package

Slide 8

Slide 8 text

# ! /bin/sh # Create an empty directory mkdir metrics # Move into the new directory cd metrics Creating a Swift Package Creating a Swift Package

Slide 9

Slide 9 text

# ! /bin/sh # Create an empty directory mkdir metrics # Move into the new directory cd metrics # Create a new executable package swift package init -- name Metrics Creating a Swift Package Creating a Swift Package

Slide 10

Slide 10 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ Metrics โ”‚ โ””โ”€โ”€ Metrics.swift โ””โ”€โ”€ Tests โ””โ”€โ”€ MetricsTests โ””โ”€โ”€ MetricsTests.swift Creating a Swift Package Creating a Swift Package # ! /bin/sh # Create an empty directory mkdir metrics # Move into the new directory cd metrics # Create a new executable package swift package init -- name Metrics

Slide 11

Slide 11 text

๐Ÿ’พ The metrics backend

Slide 12

Slide 12 text

using Swiftโ€ฆ The metrics backend ๐Ÿ’ฟ PostgresSQL database โž• POST endpoint ๐Ÿ†™ GET endpoint โŒ DELETE endpoint ๐ŸŽ‰ Built entirely with Swift ๐Ÿ’ง Using Vapor

Slide 13

Slide 13 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ ], targets: [ .target(name: "Metrics"), .testTarget(name: "MetricsTests", dependencies: ["Metrics"]) ] )

Slide 14

Slide 14 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ ], targets: [ .target(name: "Metrics"), .testTarget(name: "MetricsTests", dependencies: ["Metrics"]) ] )

Slide 15

Slide 15 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ ], targets: [ ] )

Slide 16

Slide 16 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), ], targets: [ ] )

Slide 17

Slide 17 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ], targets: [ ] )

Slide 18

Slide 18 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ], targets: [ .target( name: "App", dependencies: [ ], swiftSettings: [ ] ), ] )

Slide 19

Slide 19 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ], targets: [ .target( name: "App", dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "Vapor", package: "vapor") ], swiftSettings: [ ] ), ] )

Slide 20

Slide 20 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ], targets: [ .target( name: "App", dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "Vapor", package: "vapor") ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) ] ), ] )

Slide 21

Slide 21 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https: // github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https: // github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ], targets: [ .target( name: "App", dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "Vapor", package: "vapor") ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) ] ), .executableTarget(name: "Run", dependencies: [.target(name: "App")]) ] )

Slide 22

Slide 22 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift Modelling data

Slide 23

Slide 23 text

import Vapor import Fluent . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift Modelling data

Slide 24

Slide 24 text

import Vapor import Fluent final class Metric: Model, Content { static let schema = "metrics" } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift

Slide 25

Slide 25 text

import Vapor import Fluent final class Metric: Model, Content { static let schema = "metrics" @ID(key: .id) var id: UUID? @Field(key: "workflow") var workflow: String @Field(key: "duration") var duration: Int @Field(key: "date") var date: Date @Field(key: "repository") var repository: String @Field(key: "author") var author: String @Enum(key: "provider") var provider: Provider @Enum(key: "outcome") var outcome: Outcome init() {} } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift

Slide 26

Slide 26 text

import Vapor import Fluent enum Provider: String, Codable { case xcodeCloud = "xcode-cloud" case githubActions = "github-actions" } enum Outcome: String, Codable { case success case failure case cancelled } final class Metric: Model, Content { static let schema = "metrics" @ID(key: .id) var id: UUID? @Field(key: "workflow") var workflow: String @Field(key: "duration") var duration: Int @Field(key: "date") var date: Date @Field(key: "repository") var repository: String @Field(key: "author") var author: String @Enum(key: "provider") var provider: Provider @Enum(key: "outcome") var outcome: Outcome init() {} } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift

Slide 27

Slide 27 text

import Fluent struct CreateMetric: Migration { } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift Modelling data

Slide 28

Slide 28 text

import Fluent struct CreateMetric: Migration { func prepare(on database: Database) -> EventLoopFuture { } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift

Slide 29

Slide 29 text

import Fluent struct CreateMetric: Migration { func prepare(on database: Database) -> EventLoopFuture { database.schema("metrics") } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift

Slide 30

Slide 30 text

import Fluent struct CreateMetric: Migration { func prepare(on database: Database) -> EventLoopFuture { database.schema("metrics") .id() .field("workflow", .string, .required) .field("duration", .int, .required) .field("date", .datetime, .required) .field("provider", .string, .required) .field("outcome", .string, .required) .field("repository", .string, .required) .field("author", .string, .required) } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift

Slide 31

Slide 31 text

import Fluent struct CreateMetric: Migration { func prepare(on database: Database) -> EventLoopFuture { database.schema("metrics") .id() .field("workflow", .string, .required) .field("duration", .int, .required) .field("date", .datetime, .required) .field("provider", .string, .required) .field("outcome", .string, .required) .field("repository", .string, .required) .field("author", .string, .required) .create() } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift

Slide 32

Slide 32 text

import Fluent struct CreateMetric: Migration { func prepare(on database: Database) -> EventLoopFuture { database.schema("metrics") .id() .field("workflow", .string, .required) .field("duration", .int, .required) .field("date", .datetime, .required) .field("provider", .string, .required) .field("outcome", .string, .required) .field("repository", .string, .required) .field("author", .string, .required) .create() } func revert(on database: Database) -> EventLoopFuture { database.schema("metrics").delete() } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift

Slide 33

Slide 33 text

import Fluent import Vapor struct MetricsController: RouteCollection { } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift Defining the routes

Slide 34

Slide 34 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 35

Slide 35 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "metrics") api.get(use: get) } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 36

Slide 36 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "metrics") api.get(use: get) } func get(req: Request) async throws -> [Metric] { try await Metric.query(on: req.db) .sort(\.$date, .descending) .all() } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 37

Slide 37 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "metrics") api.get(use: get) api.get(":id", use: find) } func find(req: Request) async throws -> Metric { if let metric = try await Metric .find(req.parameters.get("id"), on: req.db) { return metric } else { throw Abort(.notFound) } } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 38

Slide 38 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "metrics") api.get(use: get) api.get(":id", use: find) api.post(use: create) } func create(req: Request) async throws -> Metric { let metric = try req.content.decode(Metric.self) try await metric.save(on: req.db) return metric } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 39

Slide 39 text

import Fluent import Vapor struct MetricsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "metrics") api.get(use: get) api.get(":id", use: find) api.post(use: create) api.delete(":id", use: delete) } func delete(req: Request) async throws -> HTTPStatus { guard let metric = try await Metric .find( req.parameters.get("id"), on: req.db ) else { throw Abort(.notFound) } try await metric.delete(on: req.db) return .noContent } } Defining the endpoints . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift

Slide 40

Slide 40 text

import Fluent import Vapor func routes(_ app: Application) throws { try app.register(collection: MetricsController()) } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift Registering the routes

Slide 41

Slide 41 text

import Fluent import FluentPostgresDriver import Vapor public func configure(_ app: Application) throws { } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift Finallyโ€ฆ

Slide 42

Slide 42 text

import Fluent import FluentPostgresDriver import Vapor public func configure(_ app: Application) throws { app.databases.use(.postgres( hostname: Environment .get("DATABASE_HOST") ?? "localhost", port: Environment .get("DATABASE_PORT") .flatMap(Int.init(_:)) ?? 5432, username: Environment .get("DATABASE_USERNAME") ?? "vapor_username", password: Environment .get("DATABASE_PASSWORD") ?? "vapor_password", database: Environment .get("DATABASE_NAME") ?? "vapor_database" ), as: .psql) } Finallyโ€ฆ . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift

Slide 43

Slide 43 text

import Fluent import FluentPostgresDriver import Vapor public func configure(_ app: Application) throws { app.databases.use(.postgres( hostname: Environment .get("DATABASE_HOST") ?? "localhost", port: Environment .get("DATABASE_PORT") .flatMap(Int.init(_:)) ?? 5432, username: Environment .get("DATABASE_USERNAME") ?? "vapor_username", password: Environment .get("DATABASE_PASSWORD") ?? "vapor_password", database: Environment .get("DATABASE_NAME") ?? "vapor_database" ), as: .psql) app.migrations.add(CreateMetric()) try app.autoMigrate().wait() } Finallyโ€ฆ . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift

Slide 44

Slide 44 text

import Fluent import FluentPostgresDriver import Vapor public func configure(_ app: Application) throws { app.databases.use(.postgres( hostname: Environment .get("DATABASE_HOST") ?? "localhost", port: Environment .get(โ€œDATABASE_PORT") .flatMap(Int.init(_:)) ?? 5432, username: Environment .get("DATABASE_USERNAME") ?? "vapor_username", password: Environment .get("DATABASE_PASSWORD") ?? "vapor_password", database: Environment .get("DATABASE_NAME") ?? "vapor_database" ), as: .psql) app.migrations.add(CreateMetric()) try app.autoMigrate().wait() app.logger.logLevel = .debug } Finallyโ€ฆ . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift

Slide 45

Slide 45 text

import Fluent import FluentPostgresDriver import Vapor public func configure(_ app: Application) throws { app.databases.use(.postgres( hostname: Environment .get("DATABASE_HOST") ?? "localhost", port: Environment .get(โ€œDATABASE_PORT") .flatMap(Int.init(_:)) ?? 5432, username: Environment .get("DATABASE_USERNAME") ?? "vapor_username", password: Environment .get("DATABASE_PASSWORD") ?? "vapor_password", database: Environment .get("DATABASE_NAME") ?? "vapor_database" ), as: .psql) app.migrations.add(CreateMetric()) try app.autoMigrate().wait() app.logger.logLevel = .debug try routes(app) } Finallyโ€ฆ . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift

Slide 46

Slide 46 text

The run target . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 47

Slide 47 text

The run target . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 48

Slide 48 text

import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() The run target . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

๐Ÿ“ˆ The metrics dashboard

Slide 51

Slide 51 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift ๐Ÿ Creating a template

Slide 52

Slide 52 text

๐Ÿ Creating a template . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 53

Slide 53 text

</ script> <script src="https: // cdn.tailwindcss.com"> </ script> </ head> <body class="h-screen w-screen bg-gray-900"> <main> <div class="mx-auto my-8 text-3xl font-semibold max-w-5xl > <section class="flex flex-col gap-2"> <h1 class="text-5xl">QReate Metrics </ h1> <p class="text-sm font-normal"> A dashboard to aggregate all workflows data for y </ p> </ section> <section> <p class="text-2xl font-semibold mb-4">All workflow </ <div id="chart"> </ div> </ section> <section> <p class="text-2xl font-semibold mb-4">Recent build </ <div class="flex flex-col gap-6"> #for(metric in metrics): <div class="flex flex-col gap-1 text-lg round ๐Ÿ Creating a template . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 54

Slide 54 text

</ script> <script src="https: // cdn.tailwindcss.com"> </ script> </ head> <body class="h-screen w-screen bg-gray-900"> <main> <div class="mx-auto my-8 text-3xl font-semibold max-w-5xl > <section class="flex flex-col gap-2"> <h1 class="text-5xl">QReate Metrics </ h1> <p class="text-sm font-normal"> A dashboard to aggregate all workflows data for y </ p> </ section> <section> <p class="text-2xl font-semibold mb-4">All workflow </ <div id="chart"> </ div> </ section> <section> <p class="text-2xl font-semibold mb-4">Recent build </ <div class="flex flex-col gap-6"> #for(metric in metrics): <div class="flex flex-col gap-1 text-lg round ๐Ÿ Creating a template . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 55

Slide 55 text

import Vapor struct FrontendController: RouteCollection { } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 56

Slide 56 text

import Vapor struct FrontendController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.get(use: get) } func get(req: Request) async throws -> View { } } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 57

Slide 57 text

import Vapor struct FrontendController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.get(use: get) } func get(req: Request) async throws -> View { let allMetrics = try await Metric.query(on: req.db) .sort(\.$date, .descending) .all() } } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 58

Slide 58 text

import Vapor struct FrontendController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.get(use: get) } func get(req: Request) async throws -> View { let allMetrics = try await Metric.query(on: req.db) .sort(\.$date, .descending) .all() return try await req.view.render( "Home", HomeContext(metrics: allMetrics) ) } } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 59

Slide 59 text

import Vapor struct FrontendController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.get(use: get) } func get(req: Request) async throws -> View { let allMetrics = try await Metric.query(on: req.db) .sort(\.$date, .descending) .all() return try await req.view.render( "Home", HomeContext(metrics: allMetrics) ) } } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 60

Slide 60 text

import Vapor struct HomeContext: Encodable { let metrics: [Metric] } struct FrontendController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.get(use: get) } func get(req: Request) async throws -> View { let allMetrics = try await Metric.query(on: req.db) .sort(\.$date, .descending) .all() return try await req.view.render( "Home", HomeContext(metrics: allMetrics) ) } } . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

Deploying a Vapor app? ๐Ÿค” Mikaela has your back! ๐ŸŽ‰

Slide 63

Slide 63 text

๐Ÿš€ GitHub Actions Metrics

Slide 64

Slide 64 text

โ€ข App has a single GHA work fl ow โ€ข Send metric points to the metrics service. โ€ข Straight after the work fl owโ€™s execution is done. โ€ข No back-end service โ€ข A work fl ow using the work fl ow_run event โ€ข Run a Swift CLI ๐Ÿš€ GitHub Actions Metrics

Slide 65

Slide 65 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ ], targets: [ ] )

Slide 66

Slide 66 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ ], targets: [ .executableTarget( name: "GithubActionsMetricsCLI", dependencies: [ ] ) ] )

Slide 67

Slide 67 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ .executable(name: "GithubActionsMetricsCLI", targets: ["GithubActionsMetricsCLI"]) ], dependencies: [ ], targets: [ .executableTarget( name: "GithubActionsMetricsCLI", dependencies: [ ] ) ] )

Slide 68

Slide 68 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ .executable(name: "GithubActionsMetricsCLI", targets: ["GithubActionsMetricsCLI"]) ], dependencies: [ .package(url: "https: // github.com/apple/swift-argument-parser.git", exact: "1.2.2") ], targets: [ .executableTarget( name: "GithubActionsMetricsCLI", dependencies: [ ] ) ] )

Slide 69

Slide 69 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ .executable(name: "GithubActionsMetricsCLI", targets: ["GithubActionsMetricsCLI"]) ], dependencies: [ .package(url: "https: // github.com/apple/swift-argument-parser.git", exact: "1.2.2") ], targets: [ .executableTarget( name: "GithubActionsMetricsCLI", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser") ] ) ] )

Slide 70

Slide 70 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 71

Slide 71 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ GithubActionsMetricsCLI โ”‚ โ”‚ โ””โ”€โ”€ GithubActionsMetricsCLI.swift โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 72

Slide 72 text

import Foundation

Slide 73

Slide 73 text

import Foundation struct GithubActionsMetricsCLI { }

Slide 74

Slide 74 text

import Foundation @main struct GithubActionsMetricsCLI { }

Slide 75

Slide 75 text

import Foundation import ArgumentParser @main struct GithubActionsMetricsCLI: AsyncParsableCommand { }

Slide 76

Slide 76 text

import Foundation import ArgumentParser @main struct GithubActionsMetricsCLI: AsyncParsableCommand { func run() async throws { } }

Slide 77

Slide 77 text

import Foundation import ArgumentParser @main struct GithubActionsMetricsCLI: AsyncParsableCommand { @Argument private var workflow: String @Argument private var updatedAt: String @Argument private var date: String @Argument private var repository: String @Argument private var outcome: String @Argument private var author: String func run() async throws { } }

Slide 78

Slide 78 text

func run() async throws { }

Slide 79

Slide 79 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } } extension Date { init?(from isoString: String) { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" if let date = dateFormatter.date(from: isoString) { self = date } else { return nil } } }

Slide 80

Slide 80 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } }

Slide 81

Slide 81 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) }

Slide 82

Slide 82 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) guard let outcome = adapt(githubActionsOutcome: outcome) else { print("Not handling the GHA outcome \(outcome.rawValue)") return } } private func adapt(githubActionsOutcome: GithubActionOutcome) -> Outcome? { switch githubActionsOutcome { case .success: return .success case .failure: return .failure case .cancelled: return .cancelled default: return nil } }

Slide 83

Slide 83 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) guard let outcome = adapt(githubActionsOutcome: outcome) else { print("Not handling the GHA outcome \(outcome.rawValue)") return } }

Slide 84

Slide 84 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) guard let outcome = adapt(githubActionsOutcome: outcome) else { print("Not handling the GHA outcome \(outcome.rawValue)") return } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return } }

Slide 85

Slide 85 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) guard let outcome = adapt(githubActionsOutcome: outcome) else { print("Not handling the GHA outcome \(outcome.rawValue)") return } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return } let payload = Payload( workflow: workflow, duration: Int(duration), date: startedAtDate, provider: .githubActions, author: author, outcome: outcome, repository: repository ) }

Slide 86

Slide 86 text

func run() async throws { guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) guard let outcome = adapt(githubActionsOutcome: outcome) else { print("Not handling the GHA outcome \(outcome.rawValue)") return } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return } let payload = Payload( workflow: workflow, duration: Int(duration), date: startedAtDate, provider: .githubActions, author: author, outcome: outcome, repository: repository ) let analyticsService = Factory.make(with: analyticsEndpoint) _ = try await analyticsService.send(payload: payload) }

Slide 87

Slide 87 text

Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 88

Slide 88 text

. โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml name: Log workflow run metrics Creating a workflow

Slide 89

Slide 89 text

name: Log workflow run metrics on: workflow_run: workflows: [Lint] types: - completed Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 90

Slide 90 text

name: Log workflow run metrics on: workflow_run: workflows: [Lint] types: - completed jobs: log: runs-on: macos-latest Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 91

Slide 91 text

name: Log workflow run metrics on: workflow_run: workflows: [Lint] types: - completed jobs: log: runs-on: macos-latest env: ANALYTICS_ENDPOINT: ${{ secrets.ANALYTICS_ENDPOINT }} Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 92

Slide 92 text

name: Log workflow run metrics on: workflow_run: workflows: [Lint] types: - completed jobs: log: runs-on: macos-latest env: ANALYTICS_ENDPOINT: ${{ secrets.ANALYTICS_ENDPOINT }} steps: - uses: actions/checkout@v3 with: repository: polpielladev/metrics Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 93

Slide 93 text

name: Log workflow run metrics on: workflow_run: workflows: [Lint] types: - completed jobs: log: runs-on: macos-latest env: ANALYTICS_ENDPOINT: ${{ secrets.ANALYTICS_ENDPOINT }} steps: - uses: actions/checkout@v3 with: repository: polpielladev/metrics - run: | swift run GithubActionsMetricsCLI \ "${{ github.event.workflow_run.name }}" \ "${{ github.event.workflow_run.updated_at }}" \ "${{ github.event.workflow_run.run_started_at }}" \ "${{ github.event.workflow_run.head_repository.name }}" \ "${{ github.event.workflow_run.conclusion }}" \ "${{ github.event.workflow_run.head_commit.author.name }}" Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

Another example Used by the Swift Package Index project to generate Pull Requests from issues using webhook logic directly in GHA work fl ows.

Slide 96

Slide 96 text

Learn more I have an article on the topic where you can fi nd a more in- depth explanation of this section of the slides.

Slide 97

Slide 97 text

๐ŸŒง Xcode Cloud metrics

Slide 98

Slide 98 text

โ€ข Send metrics from all Xcode Cloud work fl ows. โ€ข Straight after the work fl ow is done executing. โ€ข Using an Xcode Cloud webhook. โ€ข Serverless Swift lambda. Xcode Cloud metrics

Slide 99

Slide 99 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ ], targets: [ ] )

Slide 100

Slide 100 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ .package(url: "https: // github.com/swift-server/swift-aws-lambda-runtime.git", exact: "1.0.0-alpha.1"), .package(url: "https: // github.com/swift-server/swift-aws-lambda-events.git", exact: "0.1.0") ], targets: [ ] )

Slide 101

Slide 101 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ .package(url: "https: // github.com/swift-server/swift-aws-lambda-runtime.git", exact: "1.0.0-alpha.1"), .package(url: "https: // github.com/swift-server/swift-aws-lambda-events.git", exact: "0.1.0") ], targets: [ .executableTarget( name: "XcodeCloudWebhook", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events") ] ) ] )

Slide 102

Slide 102 text

// swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics", platforms: [ .macOS(.v12) ], products: [ .executable(name: "XcodeCloudWebhook", targets: ["XcodeCloudWebhook"]) ], dependencies: [ .package(url: "https: // github.com/swift-server/swift-aws-lambda-runtime.git", exact: "1.0.0-alpha.1"), .package(url: "https: // github.com/swift-server/swift-aws-lambda-events.git", exact: "0.1.0") ], targets: [ .executableTarget( name: "XcodeCloudWebhook", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events") ] ) ] )

Slide 103

Slide 103 text

. โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Resources โ”‚ โ””โ”€โ”€ Views โ”‚ โ””โ”€โ”€ Home.leaf โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ XcodeCloudWebhook โ”‚ โ”‚ โ””โ”€โ”€ XcodeCloudWebhook.swift โ”‚ โ””โ”€โ”€ GithubActionsMetricsCLI โ”‚ โ”‚ โ””โ”€โ”€ GithubActionsMetricsCLI.swift โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ”‚ โ””โ”€โ”€ FrontendController.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift

Slide 104

Slide 104 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation

Slide 105

Slide 105 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation struct XcodeCloudWebhook: SimpleLambdaHandler { }

Slide 106

Slide 106 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation @main struct XcodeCloudWebhook: SimpleLambdaHandler { }

Slide 107

Slide 107 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation @main struct XcodeCloudWebhook: SimpleLambdaHandler { init() { } }

Slide 108

Slide 108 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation @main struct XcodeCloudWebhook: SimpleLambdaHandler { let decoder: JSONDecoder init() { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(dateFormatter) self.decoder = decoder } }

Slide 109

Slide 109 text

import AWSLambdaRuntime import AWSLambdaEvents import Foundation @main struct XcodeCloudWebhook: SimpleLambdaHandler { let decoder: JSONDecoder init() { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(dateFormatter) self.decoder = decoder } func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { } }

Slide 110

Slide 110 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { }

Slide 111

Slide 111 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { guard let body = request.body, let bodyData = body.data(using: .utf8), let payload = try? decoder.decode(WebhookPayload.self, from: bodyData) else { return .init(statusCode: .ok, body: "Not handling the request ... ") } }

Slide 112

Slide 112 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { guard let body = request.body, let bodyData = body.data(using: .utf8), let payload = try? decoder.decode(WebhookPayload.self, from: bodyData), payload.ciBuildRun.attributes.executionProgress == "COMPLETE" else { return .init(statusCode: .ok, body: "Not handling the request ... ") } }

Slide 113

Slide 113 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { guard let body = request.body, let bodyData = body.data(using: .utf8), let payload = try? decoder.decode(WebhookPayload.self, from: bodyData), payload.ciBuildRun.attributes.executionProgress == "COMPLETE" else { return .init(statusCode: .ok, body: "Not handling the request ... ") } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return .init(statusCode: .internalServerError) } }

Slide 114

Slide 114 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { guard let body = request.body, let bodyData = body.data(using: .utf8), let payload = try? decoder.decode(WebhookPayload.self, from: bodyData), payload.ciBuildRun.attributes.executionProgress == "COMPLETE" else { return .init(statusCode: .ok, body: "Not handling the request ... ") } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return .init(statusCode: .internalServerError) } let analyticsService = Factory.make(with: analyticsEndpoint) }

Slide 115

Slide 115 text

func handle(_ request: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { guard let body = request.body, let bodyData = body.data(using: .utf8), let payload = try? decoder.decode(WebhookPayload.self, from: bodyData), payload.ciBuildRun.attributes.executionProgress == "COMPLETE" else { return .init(statusCode: .ok, body: "Not handling the request ... ") } guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return .init(statusCode: .internalServerError) } let analyticsService = Factory.make(with: analyticsEndpoint) guard let startDate = payload.ciBuildRun.attributes.startedDate, let duration = payload .ciBuildRun .attributes .finishedDate? .timeIntervalSince(startDate), let outcome = Self.adapt(xcodeCloudOutcome: payload.ciBuildRun.attributes.completionStatus) else { return .init(statusCode: .ok, body: "Not handling this request ... ") } }

Slide 116

Slide 116 text

guard let analyticsEndpoint = ProcessInfo.processInfo.environment["ANALYTICS_ENDPOINT"] else { print("ANALYTICS_ENDPOINT is not set. Skipping sending analytics.") return .init(statusCode: .internalServerError) } let analyticsService = Factory.make(with: analyticsEndpoint) guard let startDate = payload.ciBuildRun.attributes.startedDate, let duration = payload .ciBuildRun .attributes .finishedDate? .timeIntervalSince(startDate), let outcome = Self.adapt(xcodeCloudOutcome: payload.ciBuildRun.attributes.completionStatus) else { return .init(statusCode: .ok, body: "Not handling this request ... ") } let analyticsPayload = Payload( workflow: payload.ciWorkflow.attributes.name, duration: Int(duration), date: startDate, provider: .xcodeCloud, author: payload.ciBuildRun.attributes.sourceCommit.author.displayName, outcome: outcome, repository: payload.scmRepository.attributes.repositoryName ) _ = try await analyticsService.send(payload: analyticsPayload) return .init(statusCode: .ok) }

Slide 117

Slide 117 text

Letโ€™s deploy the lambda! swift package \ --disable-sandbox archive \ --swift-version 5.7 \ --output-path . \ --products XcodeCloudWebhook

Slide 118

Slide 118 text

Letโ€™s deploy the lambda! swift package \ --disable-sandbox archive \ --swift-version 5.7 \ --output-path . \ --products XcodeCloudWebhook

Slide 119

Slide 119 text

Letโ€™s deploy the lambda! swift package \ --disable-sandbox archive \ --swift-version 5.7 \ --output-path . \ --products XcodeCloudWebhook

Slide 120

Slide 120 text

Letโ€™s deploy the lambda!

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

Learn more I have an article on the topic where you can fi nd a more in- depth explanation of this section of this section.

Slide 123

Slide 123 text

๐ŸŽ Letโ€™s wrap up! โš™ A full-stack website using Vapor ๐ŸŽ An API using Vapor ๐Ÿ’ป A command line tool for macOS, extensible to other platforms too! ๐Ÿ”— A serverless Swift lambda using AWS ๐Ÿ“ฆ Everything in the same Swift Package! โค Swift is great for developer tooling! ๐Ÿค” Great way to upskill!

Slide 124

Slide 124 text

polpiella.dev @polpielladev iOS CI Newsle tt er Say hi! ๐Ÿ‘‹