From d10dcf4fb3052e1f0c799cbff7b0e6b9ccd0cf05 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 19 May 2024 17:58:57 +0000 Subject: [PATCH] Bootstrapping development with some basic types and builds. (#1) Reviewed-on: https://git.garrity.co/garrity-software/smolban/pulls/1 --- .forgejo/workflows/pull_request.yaml | 40 ++++++++ .forgejo/workflows/release.yaml | 53 ++++++++++ .gitignore | 4 + .pre-commit-config.yaml | 16 +++ .scalafmt.conf | 72 ++++++++++++++ README.md | 2 +- build.sbt | 98 +++++++++++++++++++ .../scala/gs/smolban/model/CreatedAt.scala | 34 +++++++ .../main/scala/gs/smolban/model/Group.scala | 54 ++++++++++ .../main/scala/gs/smolban/model/Ticket.scala | 44 +++++++++ project/build.properties | 1 + project/plugins.sbt | 33 +++++++ 12 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/pull_request.yaml create mode 100644 .forgejo/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .scalafmt.conf create mode 100644 build.sbt create mode 100644 modules/model/src/main/scala/gs/smolban/model/CreatedAt.scala create mode 100644 modules/model/src/main/scala/gs/smolban/model/Group.scala create mode 100644 modules/model/src/main/scala/gs/smolban/model/Ticket.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt diff --git a/.forgejo/workflows/pull_request.yaml b/.forgejo/workflows/pull_request.yaml new file mode 100644 index 0000000..ef3461d --- /dev/null +++ b/.forgejo/workflows/pull_request.yaml @@ -0,0 +1,40 @@ +on: + pull_request: + types: [opened, synchronize, reopened] + +defaults: + run: + shell: bash + +jobs: + library_snapshot: + runs-on: docker + container: + image: registry.garrity.co:8443/gs/ci-scala:latest + name: 'Build and Test Application Snapshot' + env: + GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }} + GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }} + steps: + - uses: actions/checkout@v4 + name: 'Checkout Repository' + with: + fetch-depth: 0 + - name: 'Pre-Commit' + run: | + pre-commit install + pre-commit run --all-files + - name: 'Prepare Versioned Build' + run: | + latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')" + latest_commit_message="$(git show -s --format=%s HEAD)" + echo "Previous Git Tag: $latest_git_tag" + echo "Latest Commit: $latest_commit_message (SNAPSHOT)" + sbtn -Dsnapshot=true "api/calVerInfo" + - name: 'Unit Tests and Code Coverage' + run: | + sbtn clean + sbtn coverage + sbtn test + sbtn coverageReport + sbtn coverageAggregate diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml new file mode 100644 index 0000000..618672a --- /dev/null +++ b/.forgejo/workflows/release.yaml @@ -0,0 +1,53 @@ +on: + push: + branches: + - main + +defaults: + run: + shell: bash + +jobs: + application_release: + runs-on: docker + container: + image: registry.garrity.co:8443/gs/ci-scala:latest + name: 'Build and Release Application' + env: + GS_MAVEN_USER: ${{ vars.GS_MAVEN_USER }} + GS_MAVEN_TOKEN: ${{ secrets.GS_MAVEN_TOKEN }} + steps: + - uses: actions/checkout@v4 + name: 'Checkout Repository' + with: + fetch-depth: 0 + - name: 'Pre-Commit' + run: | + pre-commit install + pre-commit run --all-files + - name: 'Prepare Versioned Build' + run: | + latest_git_tag="$(git describe --tags --abbrev=0 || echo 'No Tags')" + latest_commit_message="$(git show -s --format=%s HEAD)" + echo "Previous Git Tag: $latest_git_tag" + echo "Latest Commit: $latest_commit_message" + sbtn -Drelease=true api/calVerInfo + - name: 'Unit Tests and Code Coverage' + run: | + sbtn clean + sbtn coverage + sbtn test + sbtn coverageReport + sbtn coverageAggregate + - name: 'Publish Release' + run: | + sbtn coverageOff + sbtn clean + sbtn api/calVerWriteVersionToFile + sbtn compile + - name: 'Create Git Tag' + run: | + selected_version="$(cat .version)" + echo "TAGGING IS CURRENTLY TURNED OFF -- $selected_version" + #git tag "$selected_version" + #git push origin "$selected_version" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e79824 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +project/target/ +project/project/ +.version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fbcf79c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: fix-byte-order-marker + - id: mixed-line-ending + args: ['--fix=lf'] + description: Enforces using only 'LF' line endings. + - id: trailing-whitespace + - repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala + rev: v1.0.1 + hooks: + - id: scalafmt diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..9c7929b --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,72 @@ +// See: https://github.com/scalameta/scalafmt/tags for the latest tags. +version = 3.8.1 +runner.dialect = scala3 +maxColumn = 80 + +rewrite { + rules = [RedundantBraces, RedundantParens, Imports, SortModifiers] + imports.expand = true + imports.sort = scalastyle + redundantBraces.ifElseExpressions = true + redundantBraces.stringInterpolation = true +} + +indent { + main = 2 + callSite = 2 + defnSite = 2 + extendSite = 4 + withSiteRelativeToExtends = 2 + commaSiteRelativeToExtends = 2 +} + +align { + preset = more + openParenCallSite = false + openParenDefnSite = false +} + +newlines { + implicitParamListModifierForce = [before,after] + topLevelStatementBlankLines = [ + { + blanks = 1 + } + ] + afterCurlyLambdaParams = squash +} + +danglingParentheses { + defnSite = true + callSite = true + ctrlSite = true + exclude = [] +} + +verticalMultiline { + atDefnSite = true + arityThreshold = 2 + newlineAfterOpenParen = true +} + +comments { + wrap = standalone +} + +docstrings { + style = "SpaceAsterisk" + oneline = unfold + wrap = yes + forceBlankLineBefore = true +} + +project { + excludePaths = [ + "glob:**target/**", + "glob:**.metals/**", + "glob:**.bloop/**", + "glob:**.bsp/**", + "glob:**metals.sbt", + "glob:**.git/**" + ] +} diff --git a/README.md b/README.md index 7ec75ac..bd4a1a2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Smol Kanban - dead simple tickets. ## Why Smolban? I created smolban after looking for a free issue tracker that suited my needs. -I tend to prefer feature-light, and didn't want to install something larger +I tend to prefer feature-light, and didn't want to install something larger or more "agile" focused. I explicitly do not want to use VCS issues. Smolban is a self-contained issue tracker intended to be easy to deploy and use. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..43a1894 --- /dev/null +++ b/build.sbt @@ -0,0 +1,98 @@ +val scala3: String = "3.4.1" + +ThisBuild / scalaVersion := scala3 +ThisBuild / gsProjectName := "smolban" + +ThisBuild / externalResolvers := Seq( + "Garrity Software Mirror" at "https://maven.garrity.co/releases", + "Garrity Software Releases" at "https://maven.garrity.co/gs" +) + +lazy val sharedSettings = Seq( + scalaVersion := scala3, + version := calVer.value, + publish / skip := true, + publishLocal / skip := true, + publishArtifact := false +) + +val Deps = new { + val Cats = new { + val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.10.0" + val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.5.4" + } + + val Fs2 = new { + val Core: ModuleID = "co.fs2" %% "fs2-core" % "3.10.2" + } + + val Doobie = new { + val Core: ModuleID = "org.tpolecat" %% "doobie-core" % "1.0.0-M5" + } + + val Http4s = new { + val Core: ModuleID = "org.http4s" %% "http4s-core" % "1.0.0-M41" + val Dsl: ModuleID = "org.http4s" %% "http4s-dsl" % "1.0.0-M41" + val EmberServer: ModuleID = + "org.http4s" %% "http4s-ember-server" % "1.0.0-M41" + } + + val Gs = new { + val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.2.4" + val Slug: ModuleID = "gs" %% "gs-slug-v0" % "0.1.3" + val Config: ModuleID = "gs" %% "gs-config-v0" % "0.1.1" + val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.1.1" + } + + val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.0.0-RC1" +} + +lazy val testSettings = Seq( + libraryDependencies ++= Seq( + Deps.MUnit % Test, + Deps.Gs.Datagen % Test + ) +) + +lazy val smolban = project + .in(file(".")) + .aggregate(model, db, api) + .settings(sharedSettings) + .settings(name := s"${gsProjectName.value}") + +lazy val model = project + .in(file("modules/model")) + .settings(sharedSettings) + .settings(testSettings) + .settings(name := s"${gsProjectName.value}-model") + .settings( + libraryDependencies ++= Seq( + Deps.Gs.Uuid, + Deps.Gs.Slug, + Deps.Cats.Core + ) + ) + +lazy val db = project + .in(file("modules/db")) + .settings(sharedSettings) + .settings(testSettings) + .settings(name := s"${gsProjectName.value}-db") + .settings( + libraryDependencies ++= Seq( + Deps.Doobie.Core + ) + ) + +lazy val api = project + .in(file("modules/api")) + .settings(sharedSettings) + .settings(testSettings) + .settings(name := s"${gsProjectName.value}-api") + .settings( + libraryDependencies ++= Seq( + Deps.Http4s.Core, + Deps.Http4s.Dsl, + Deps.Http4s.EmberServer + ) + ) diff --git a/modules/model/src/main/scala/gs/smolban/model/CreatedAt.scala b/modules/model/src/main/scala/gs/smolban/model/CreatedAt.scala new file mode 100644 index 0000000..085842a --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/CreatedAt.scala @@ -0,0 +1,34 @@ +package gs.smolban.model + +import cats.Show +import java.time.Instant + +/** Describes an instant at which something was created. Opaque type for + * `java.time.Instant`. + */ +opaque type CreatedAt = Instant + +object CreatedAt: + + /** Instantiate a new [[CreatedAt]]. + * + * @param timestamp + * The underlying timestamp. + * @return + * The new instance. + */ + def apply(timestamp: Instant): CreatedAt = timestamp + + given CanEqual[CreatedAt, CreatedAt] = CanEqual.derived + + given Show[CreatedAt] = _.toInstant().toString() + + extension (createdAt: CreatedAt) + /** Unwrap this value. + * + * @return + * The underlying `Instant` value. + */ + def toInstant(): Instant = createdAt + +end CreatedAt diff --git a/modules/model/src/main/scala/gs/smolban/model/Group.scala b/modules/model/src/main/scala/gs/smolban/model/Group.scala new file mode 100644 index 0000000..3b66350 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/Group.scala @@ -0,0 +1,54 @@ +package gs.smolban.model + +import cats.Show +import gs.slug.v0.Slug +import gs.uuid.v0.UUID + +/** Groups are the basic unit of organization in Smolban. Each [[Ticket]] + * belongs to a single `Group`. + * + * @param id + * The unique identifier for the group. + * @param slug + * The unique slug for the group. + * @param createdAt + * The instant at which this group was created. + */ +case class Group( + id: Group.Id, + slug: Slug, + createdAt: CreatedAt +) + +object Group: + + /** Unique identifier for a [[Group]]. This is an opaque type for a UUID. + */ + opaque type Id = UUID + + object Id: + + /** Instantiate a new [[Group.Id]]. + * + * @param id + * The underlying UUID. + * @return + * The new [[Group.Id]] instance. + */ + def apply(id: UUID): Id = id + + given CanEqual[Id, Id] = CanEqual.derived + + given Show[Id] = _.toUUID().withoutDashes() + + extension (id: Id) + /** Unwrap this Group ID. + * + * @return + * The underlying UUID value. + */ + def toUUID(): UUID = id + + end Id + +end Group diff --git a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala new file mode 100644 index 0000000..f373bef --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala @@ -0,0 +1,44 @@ +package gs.smolban.model + +import cats.Show + +case class Ticket( + id: Long, + group: Group.Id, + createdAt: CreatedAt +) + +object Ticket: + + /** Unique identifier - relative to some [[Group]] - for a [[Ticket]]. This is + * an opaque type for a Long. In general, [[Ticket]] identifiers are + * sequences within a group. + */ + opaque type Id = Long + + object Id: + + /** Instantiate a new [[Ticket.Id]]. + * + * @param id + * The underlying Long. + * @return + * The new [[Ticket.Id]] instance. + */ + def apply(id: Long): Id = id + + given CanEqual[Id, Id] = CanEqual.derived + + given Show[Id] = _.toLong().toString() + + extension (id: Id) + /** Unwrap this Ticket ID. + * + * @return + * The underlying Long value. + */ + def toLong(): Long = id + + end Id + +end Ticket diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..081fdbb --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.0 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..776be07 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,33 @@ +def selectCredentials(): Credentials = + if ((Path.userHome / ".sbt" / ".credentials").exists()) + Credentials(Path.userHome / ".sbt" / ".credentials") + else + Credentials.apply( + realm = "Reposilite", + host = "maven.garrity.co", + userName = sys.env + .get("GS_MAVEN_USER") + .getOrElse( + throw new RuntimeException( + "You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_USER environment variable." + ) + ), + passwd = sys.env + .get("GS_MAVEN_TOKEN") + .getOrElse( + throw new RuntimeException( + "You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_TOKEN environment variable." + ) + ) + ) + +credentials += selectCredentials() + +externalResolvers := Seq( + "Garrity Software Mirror" at "https://maven.garrity.co/releases", + "Garrity Software Releases" at "https://maven.garrity.co/gs" +) + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") +addSbtPlugin("gs" % "sbt-garrity-software" % "0.2.0") +addSbtPlugin("gs" % "sbt-gs-calver" % "0.2.0")