Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Swift Heroes 2023] Making developer tools with Swift

[Swift Heroes 2023] Making developer tools with Swift

Pol Piella Abadia

May 06, 2023
Tweet

More Decks by Pol Piella Abadia

Other Decks in Programming

Transcript

  1. 👋 Pol Piella Abadia


    🇮🇹 Swift Heroes 2023
    Making developer
    tools with Swift

    View full-size slide

  2. 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

    View full-size slide

  3. 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! ❤

    View full-size slide

  4. 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!

    View full-size slide

  5. 📦


    Creating a Swift
    Package

    View full-size slide

  6. #
    !
    /bin/sh
    Creating a Swift Package

    View full-size slide

  7. #
    !
    /bin/sh


    # Create an empty directory


    mkdir metrics
    Creating a Swift Package
    Creating a Swift Package

    View full-size slide

  8. #
    !
    /bin/sh


    # Create an empty directory


    mkdir metrics


    # Move into the new directory


    cd metrics
    Creating a Swift Package
    Creating a Swift Package

    View full-size slide

  9. #
    !
    /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

    View full-size slide

  10. .


    ├── 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

    View full-size slide

  11. 💾


    The metrics
    backend

    View full-size slide

  12. using Swift…
    The metrics backend
    💿 PostgresSQL database


    ➕ POST endpoint


    🆙 GET endpoint


    ❌ DELETE endpoint


    🎉 Built entirely with Swift


    💧 Using Vapor

    View full-size slide

  13. //
    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"])


    ]


    )


    View full-size slide

  14. //
    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"])


    ]


    )


    View full-size slide

  15. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    dependencies: [


    ],


    targets: [


    ]


    )


    View full-size slide

  16. //
    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: [


    ]


    )


    View full-size slide

  17. //
    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: [


    ]


    )


    View full-size slide

  18. //
    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: [


    ]


    ),


    ]


    )


    View full-size slide

  19. //
    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: [


    ]


    ),


    ]


    )


    View full-size slide

  20. //
    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))


    ]


    ),


    ]


    )


    View full-size slide

  21. //
    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")])


    ]


    )


    View full-size slide

  22. .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    Modelling data

    View full-size slide

  23. import Vapor


    import Fluent
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    Modelling data

    View full-size slide

  24. import Vapor


    import Fluent


    final class Metric: Model, Content {


    static let schema = "metrics"


    }
    Modelling data
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    View full-size slide

  25. 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


    View full-size slide

  26. 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


    View full-size slide

  27. import Fluent


    struct CreateMetric: Migration {


    }


    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    │ └── Migrations.swift


    Modelling data

    View full-size slide

  28. import Fluent


    struct CreateMetric: Migration {


    func prepare(on database: Database)
    ->
    EventLoopFuture {


    }


    }
    Modelling data
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    │ └── Migrations.swift


    View full-size slide

  29. 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


    View full-size slide

  30. 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


    View full-size slide

  31. 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


    View full-size slide

  32. 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


    View full-size slide

  33. import Fluent


    import Vapor


    struct MetricsController: RouteCollection {


    }


    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    │ └── Migrations.swift


    │ └── MetricsController.swift


    Defining the routes

    View full-size slide

  34. 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


    View full-size slide

  35. 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


    View full-size slide

  36. 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


    View full-size slide

  37. 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


    View full-size slide

  38. 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


    View full-size slide

  39. 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


    View full-size slide

  40. 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

    View full-size slide

  41. 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…

    View full-size slide

  42. 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


    View full-size slide

  43. 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


    View full-size slide

  44. 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


    View full-size slide

  45. 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


    View full-size slide

  46. The run target
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ │ └── Model.swift


    │ │ └── Migrations.swift


    │ │ └── MetricsController.swift


    │ │ └── routes.swift


    │ │ └── configure.swift


    │ └── Run


    │ └── main.swift

    View full-size slide

  47. The run target
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ │ └── Model.swift


    │ │ └── Migrations.swift


    │ │ └── MetricsController.swift


    │ │ └── routes.swift


    │ │ └── configure.swift


    │ └── Run


    │ └── main.swift

    View full-size slide

  48. 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

    View full-size slide

  49. 📈


    The metrics
    dashboard

    View full-size slide

  50. .


    ├── 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

    View full-size slide

  51. 🍁 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

    View full-size slide









  52. type="text/javascript"


    src="https:
    //
    www.gstatic.com/charts/loader.js"


    >

    script>


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

    View full-size slide









  53. type="text/javascript"


    src="https:
    //
    www.gstatic.com/charts/loader.js"


    >

    script>


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

    View full-size slide

  54. 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

    View full-size slide

  55. 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

    View full-size slide

  56. 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

    View full-size slide

  57. 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

    View full-size slide

  58. 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

    View full-size slide

  59. 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

    View full-size slide

  60. Deploying a
    Vapor app? 🤔
    Mikaela has your back! 🎉

    View full-size slide

  61. 🚀


    GitHub Actions
    Metrics

    View full-size slide

  62. • 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

    View full-size slide

  63. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    ]


    )

    View full-size slide

  64. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    .executableTarget(


    name: "GithubActionsMetricsCLI",


    dependencies: [


    ]


    )


    ]


    )

    View full-size slide

  65. //
    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: [


    ]


    )


    ]


    )

    View full-size slide

  66. //
    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: [


    ]


    )


    ]


    )

    View full-size slide

  67. //
    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")


    ]


    )


    ]


    )

    View full-size slide

  68. .


    ├── 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

    View full-size slide

  69. .


    ├── 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

    View full-size slide

  70. import Foundation

    View full-size slide

  71. import Foundation


    struct GithubActionsMetricsCLI {


    }

    View full-size slide

  72. import Foundation


    @main


    struct GithubActionsMetricsCLI {


    }

    View full-size slide

  73. import Foundation


    import ArgumentParser


    @main


    struct GithubActionsMetricsCLI: AsyncParsableCommand {


    }

    View full-size slide

  74. import Foundation


    import ArgumentParser


    @main


    struct GithubActionsMetricsCLI: AsyncParsableCommand {


    func run() async throws {


    }


    }

    View full-size slide

  75. 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 {


    }


    }

    View full-size slide

  76. func run() async throws {


    }

    View full-size slide

  77. 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


    }


    }


    }

    View full-size slide

  78. func run() async throws {


    guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else {


    return


    }


    }

    View full-size slide

  79. func run() async throws {


    guard let updateAtDate = Date(from: updatedAt), let startedAtDate = Date(from: date) else {


    return


    }




    let duration = updateAtDate.timeIntervalSince(startedAtDate)


    }

    View full-size slide

  80. 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


    }


    }

    View full-size slide

  81. 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


    }


    }

    View full-size slide

  82. 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


    }


    }

    View full-size slide

  83. 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


    )


    }

    View full-size slide

  84. 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)


    }

    View full-size slide

  85. Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View full-size slide

  86. .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml
    name: Log workflow run metrics
    Creating a workflow

    View full-size slide

  87. name: Log workflow run metrics


    on:


    workflow_run:


    workflows: [Lint]


    types:


    - completed
    Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View full-size slide

  88. 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

    View full-size slide

  89. 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

    View full-size slide

  90. 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

    View full-size slide

  91. 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

    View full-size slide

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

    View full-size slide

  93. 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.

    View full-size slide

  94. 🌧


    Xcode Cloud
    metrics

    View full-size slide

  95. • 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

    View full-size slide

  96. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    ]


    )


    View full-size slide

  97. //
    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: [


    ]


    )


    View full-size slide

  98. //
    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")


    ]


    )


    ]


    )


    View full-size slide

  99. //
    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")


    ]


    )


    ]


    )


    View full-size slide

  100. .


    ├── 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

    View full-size slide

  101. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation

    View full-size slide

  102. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    }

    View full-size slide

  103. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    @main


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    }

    View full-size slide

  104. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    @main


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    init() {


    }


    }

    View full-size slide

  105. 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


    }


    }

    View full-size slide

  106. 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 {


    }


    }

    View full-size slide

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


    }


    View full-size slide

  108. 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
    ...
    ")


    }


    }


    View full-size slide

  109. 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
    ...
    ")


    }


    }


    View full-size slide

  110. 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)


    }


    }

    View full-size slide

  111. 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)


    }

    View full-size slide

  112. 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
    ...
    ")


    }


    }


    View full-size slide

  113. 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)


    }


    View full-size slide

  114. Let’s deploy the lambda!
    swift package \


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View full-size slide

  115. Let’s deploy the lambda!
    swift package \


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View full-size slide

  116. Let’s deploy the lambda!
    swift package \


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View full-size slide

  117. Let’s deploy the lambda!

    View full-size slide

  118. 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.

    View full-size slide

  119. 🎁 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!

    View full-size slide

  120. polpiella.dev
    @polpielladev iOS CI Newsle
    tt
    er
    Say hi! 👋

    View full-size slide