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 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 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 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 Slide

  5. 📦


    Creating a Swift
    Package

    View Slide

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

    View Slide

  7. #
    !
    /bin/sh


    # Create an empty directory


    mkdir metrics
    Creating a Swift Package
    Creating a Swift Package

    View 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 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 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 Slide

  11. 💾


    The metrics
    backend

    View Slide

  12. using Swift…
    The metrics backend
    💿 PostgresSQL database


    ➕ POST endpoint


    🆙 GET endpoint


    ❌ DELETE endpoint


    🎉 Built entirely with Swift


    💧 Using Vapor

    View 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 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 Slide

  15. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    dependencies: [


    ],


    targets: [


    ]


    )


    View 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 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 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 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 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 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 Slide

  22. .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    Modelling data

    View Slide

  23. import Vapor


    import Fluent
    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    Modelling data

    View 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 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 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 Slide

  27. import Fluent


    struct CreateMetric: Migration {


    }


    .


    ├── Package.swift


    ├── README.md


    ├── Sources


    │ └── App


    │ └── Model.swift


    │ └── Migrations.swift


    Modelling data

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

  49. View Slide

  50. 📈


    The metrics
    dashboard

    View Slide

  51. .


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

  52. 🍁 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 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 Slide









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

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

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


    }


    }
    .


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

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

  60. 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 Slide

  61. View Slide

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

    View Slide

  63. 🚀


    GitHub Actions
    Metrics

    View Slide

  64. • 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 Slide

  65. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    ]


    )

    View Slide

  66. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    .executableTarget(


    name: "GithubActionsMetricsCLI",


    dependencies: [


    ]


    )


    ]


    )

    View Slide

  67. //
    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 Slide

  68. //
    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 Slide

  69. //
    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 Slide

  70. .


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

  71. .


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

  72. import Foundation

    View Slide

  73. import Foundation


    struct GithubActionsMetricsCLI {


    }

    View Slide

  74. import Foundation


    @main


    struct GithubActionsMetricsCLI {


    }

    View Slide

  75. import Foundation


    import ArgumentParser


    @main


    struct GithubActionsMetricsCLI: AsyncParsableCommand {


    }

    View Slide

  76. import Foundation


    import ArgumentParser


    @main


    struct GithubActionsMetricsCLI: AsyncParsableCommand {


    func run() async throws {


    }


    }

    View Slide

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

  78. func run() async throws {


    }

    View Slide

  79. 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 Slide

  80. func run() async throws {


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


    return


    }


    }

    View Slide

  81. func run() async throws {


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


    return


    }




    let duration = updateAtDate.timeIntervalSince(startedAtDate)


    }

    View 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


    }


    }


    private func adapt(githubActionsOutcome: GithubActionOutcome)
    ->
    Outcome? {


    switch githubActionsOutcome {


    case .success: return .success


    case .failure: return .failure


    case .cancelled: return .cancelled


    default: return nil


    }


    }

    View 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


    }


    }

    View 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


    }


    }

    View Slide

  85. 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 Slide

  86. 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 Slide

  87. Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View Slide

  88. .


    ├── .github


    │ └── workflows


    │ └── lint.yml


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

    View Slide

  89. name: Log workflow run metrics


    on:


    workflow_run:


    workflows: [Lint]


    types:


    - completed
    Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View Slide

  90. 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 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 }}
    Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View Slide

  92. 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/[email protected]


    with:


    repository: polpielladev/metrics
    Creating a workflow
    .


    ├── .github


    │ └── workflows


    │ └── lint.yml


    │ └── workflow_run.yml

    View Slide

  93. 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/[email protected]


    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 Slide

  94. View Slide

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

  96. 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 Slide

  97. 🌧


    Xcode Cloud
    metrics

    View Slide

  98. • 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 Slide

  99. //
    swift-tools-version:5.6


    import PackageDescription


    let package = Package(


    name: "Metrics",


    platforms: [


    .macOS(.v12)


    ],


    products: [


    ],


    dependencies: [


    ],


    targets: [


    ]


    )


    View Slide

  100. //
    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 Slide

  101. //
    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 Slide

  102. //
    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 Slide

  103. .


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

  104. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation

    View Slide

  105. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    }

    View Slide

  106. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    @main


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    }

    View Slide

  107. import AWSLambdaRuntime


    import AWSLambdaEvents


    import Foundation


    @main


    struct XcodeCloudWebhook: SimpleLambdaHandler {


    init() {


    }


    }

    View Slide

  108. 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 Slide

  109. 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 Slide

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


    }


    View 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) else {


    return .init(statusCode: .ok, body: "Not handling the request
    ...
    ")


    }


    }


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


    }


    }


    View Slide

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

  114. 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 Slide

  115. 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 Slide

  116. 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 Slide

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


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View Slide

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


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View Slide

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


    --disable-sandbox archive \


    --swift-version 5.7 \


    --output-path . \


    --products XcodeCloudWebhook


    View Slide

  120. Let’s deploy the lambda!

    View Slide

  121. View Slide

  122. 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 Slide

  123. 🎁 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 Slide

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

    View Slide