Upgrade to PRO for Only $50/Yearโ€”Limited-Time Offer! ๐Ÿ”ฅ

[Swift Heroes 2023] Making developer tools with...

[Swift Heroes 2023] Making developer tools withย Swift

Avatar for Pol Piella Abadia

Pol Piella Abadia

May 06, 2023
Tweet

More Decks by Pol Piella Abadia

Other Decks in Programming

Transcript

  1. 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
  2. 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! โค
  3. 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!
  4. # ! /bin/sh # Create an empty directory mkdir metrics

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

    # Move into the new directory cd metrics Creating a Swift Package Creating a Swift Package
  6. # ! /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
  7. . โ”œโ”€โ”€ 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
  8. using Swiftโ€ฆ The metrics backend ๐Ÿ’ฟ PostgresSQL database โž• POST

    endpoint ๐Ÿ†™ GET endpoint โŒ DELETE endpoint ๐ŸŽ‰ Built entirely with Swift ๐Ÿ’ง Using Vapor
  9. // 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"]) ] )
  10. // 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"]) ] )
  11. // swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics",

    platforms: [ .macOS(.v12) ], dependencies: [ ], targets: [ ] )
  12. // 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: [ ] )
  13. // 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: [ ] )
  14. // 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: [ ] ), ] )
  15. // 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: [ ] ), ] )
  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"), .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)) ] ), ] )
  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: [ .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")]) ] )
  18. import Vapor import Fluent . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€

    Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift Modelling data
  19. import Vapor import Fluent final class Metric: Model, Content {

    static let schema = "metrics" } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift
  20. 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
  21. 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
  22. import Fluent struct CreateMetric: Migration { } . โ”œโ”€โ”€ Package.swift

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

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

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

    -> EventLoopFuture<Void> { 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
  26. import Fluent struct CreateMetric: Migration { func prepare(on database: Database)

    -> EventLoopFuture<Void> { 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
  27. import Fluent struct CreateMetric: Migration { func prepare(on database: Database)

    -> EventLoopFuture<Void> { 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<Void> { database.schema("metrics").delete() } } Modelling data . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift
  28. import Fluent import Vapor struct MetricsController: RouteCollection { } .

    โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources โ”‚ โ””โ”€โ”€ App โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ””โ”€โ”€ MetricsController.swift Defining the routes
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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โ€ฆ
  37. 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
  38. 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
  39. 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
  40. 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
  41. The run target . โ”œโ”€โ”€ Package.swift โ”œโ”€โ”€ README.md โ”œโ”€โ”€ Sources

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

    โ”‚ โ””โ”€โ”€ App โ”‚ โ”‚ โ””โ”€โ”€ Model.swift โ”‚ โ”‚ โ””โ”€โ”€ Migrations.swift โ”‚ โ”‚ โ””โ”€โ”€ MetricsController.swift โ”‚ โ”‚ โ””โ”€โ”€ routes.swift โ”‚ โ”‚ โ””โ”€โ”€ configure.swift โ”‚ โ””โ”€โ”€ Run โ”‚ โ””โ”€โ”€ main.swift
  43. 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
  44. . โ”œโ”€โ”€ 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
  45. ๐Ÿ 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
  46. <html> <head> <script type="text/javascript" src="https: // www.gstatic.com/charts/loader.js" > </ script>

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

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

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

    platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ ], targets: [ .executableTarget( name: "GithubActionsMetricsCLI", dependencies: [ ] ) ] )
  57. // 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: [ ] ) ] )
  58. // 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: [ ] ) ] )
  59. // 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") ] ) ] )
  60. . โ”œโ”€โ”€ 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
  61. . โ”œโ”€โ”€ 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
  62. 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 { } }
  63. 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 } } }
  64. func run() async throws { guard let updateAtDate = Date(from:

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

    updatedAt), let startedAtDate = Date(from: date) else { return } let duration = updateAtDate.timeIntervalSince(startedAtDate) }
  66. 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 } }
  67. 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 } }
  68. 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 } }
  69. 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 ) }
  70. 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) }
  71. Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚

    โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml
  72. . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚

    โ””โ”€โ”€ workflow_run.yml name: Log workflow run metrics Creating a workflow
  73. name: Log workflow run metrics on: workflow_run: workflows: [Lint] types:

    - completed Creating a workflow . โ”œโ”€โ”€ .github โ”‚ โ””โ”€โ”€ workflows โ”‚ โ””โ”€โ”€ lint.yml โ”‚ โ””โ”€โ”€ workflow_run.yml
  74. 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
  75. 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
  76. 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
  77. 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
  78. Another example Used by the Swift Package Index project to

    generate Pull Requests from issues using webhook logic directly in GHA work fl ows.
  79. 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.
  80. โ€ข 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
  81. // swift-tools-version:5.6 import PackageDescription let package = Package( name: "Metrics",

    platforms: [ .macOS(.v12) ], products: [ ], dependencies: [ ], targets: [ ] )
  82. // 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: [ ] )
  83. // 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") ] ) ] )
  84. // 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") ] ) ] )
  85. . โ”œโ”€โ”€ 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
  86. 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 } }
  87. 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 { } }
  88. 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 ... ") } }
  89. 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 ... ") } }
  90. 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) } }
  91. 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) }
  92. 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 ... ") } }
  93. 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) }
  94. Letโ€™s deploy the lambda! swift package \ --disable-sandbox archive \

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

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

    --swift-version 5.7 \ --output-path . \ --products XcodeCloudWebhook
  97. 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.
  98. ๐ŸŽ 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!