diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b177da0f..6638079f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,13 @@ env: workflow_dispatch: {} release: types: - - published - push: - branches: - - main + - published pull_request: branches-ignore: - - gh-pages + - gh-pages + push: + branches: + - main concurrency: group: ${{ github.workflow }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.run_id || github.ref }} cancel-in-progress: true @@ -24,234 +24,236 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: '17' - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Check all code compiles - run: sbt +Test/compile - - name: Check artifacts build process - run: sbt +publishLocal - - name: Check website build process - run: sbt docs/clean; sbt docs/buildWebsite + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: '17' + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Check all code compiles + run: sbt Test/compile + - name: Check artifacts build process + run: sbt publishLocal + - name: Check website build process + run: sbt docs/clean; sbt docs/buildWebsite lint: name: Lint runs-on: ubuntu-latest continue-on-error: false steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: '17' - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Check if the site workflow is up to date - run: sbt ciCheckGithubWorkflow - - name: Lint - run: sbt lint + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: '17' + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Check if the site workflow is up to date + run: sbt ciCheckGithubWorkflow + - name: Lint + run: sbt lint test: name: Test runs-on: ubuntu-latest continue-on-error: false strategy: - fail-fast: false matrix: java: - - '11' - - '17' - - '21' + - '11' + - '17' + - '21' + fail-fast: false steps: - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: ${{ matrix.java }} - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Test - run: sbt +test + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: ${{ matrix.java }} + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Test + run: sbt test update-readme: name: Update README runs-on: ubuntu-latest continue-on-error: false if: ${{ github.event_name == 'push' }} steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: '17' - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Generate Readme - run: sbt docs/generateReadme - - name: Commit Changes - run: | - git config --local user.email "zio-assistant[bot]@users.noreply.github.com" - git config --local user.name "ZIO Assistant" - git add README.md - git commit -m "Update README.md" || echo "No changes to commit" - - name: Generate Token - id: generate-token - uses: zio/generate-github-app-token@v1.0.0 - with: - app_id: ${{ secrets.APP_ID }} - app_private_key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Create Pull Request - id: cpr - uses: peter-evans/create-pull-request@v6 - with: - body: |- - Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: '17' + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Generate Readme + run: sbt docs/generateReadme + - name: Commit Changes + run: | + git config --local user.email "zio-assistant[bot]@users.noreply.github.com" + git config --local user.name "ZIO Assistant" + git add README.md + git commit -m "Update README.md" || echo "No changes to commit" + - name: Generate Token + id: generate-token + uses: zio/generate-github-app-token@v1.0.0 + with: + app_id: ${{ secrets.APP_ID }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + body: |- + Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. - I will automatically update the README.md file whenever there is new change for README.md, e.g. - - After each release, I will update the version in the installation section. - - After any changes to the "docs/index.md" file, I will update the README.md file accordingly. - branch: zio-sbt-website/update-readme - commit-message: Update README.md - token: ${{ steps.generate-token.outputs.token }} - delete-branch: true - title: Update README.md - - name: Approve PR - if: ${{ steps.cpr.outputs.pull-request-number }} - run: gh pr review "$PR_URL" --approve - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ steps.cpr.outputs.pull-request-url }} - - name: Enable Auto-Merge - if: ${{ steps.cpr.outputs.pull-request-number }} - run: gh pr merge --auto --squash "$PR_URL" || gh pr merge --squash "$PR_URL" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + I will automatically update the README.md file whenever there is new change for README.md, e.g. + - After each release, I will update the version in the installation section. + - After any changes to the "docs/index.md" file, I will update the README.md file accordingly. + branch: zio-sbt-website/update-readme + commit-message: Update README.md + token: ${{ steps.generate-token.outputs.token }} + delete-branch: true + title: Update README.md + - name: Approve PR + if: ${{ steps.cpr.outputs.pull-request-number }} + run: gh pr review "$PR_URL" --approve + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + - name: Enable Auto-Merge + if: ${{ steps.cpr.outputs.pull-request-number }} + run: gh pr merge --auto --squash "$PR_URL" || gh pr merge --squash "$PR_URL" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} ci: name: ci runs-on: ubuntu-latest continue-on-error: false needs: - - lint - - test - - build + - lint + - test + - build steps: - - name: Report Successful CI - run: echo "ci passed" + - name: Report Successful CI + run: echo "ci passed" release: name: Release runs-on: ubuntu-latest continue-on-error: false needs: - - ci + - ci if: ${{ github.event_name != 'pull_request' }} steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: '17' - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Release - run: sbt ci-release - env: - PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} - PGP_SECRET: ${{ secrets.PGP_SECRET }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: '17' + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Release + run: sbt ci-release + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + CI_RELEASE: publishSigned + CI_SNAPSHOT_RELEASE: publish release-docs: name: Release Docs runs-on: ubuntu-latest continue-on-error: false needs: - - release + - release if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Install libuv - run: sudo apt-get update && sudo apt-get install -y libuv1-dev - - name: Setup Scala - uses: actions/setup-java@v4 - with: - distribution: corretto - java-version: '17' - check-latest: true - - name: Cache Dependencies - uses: coursier/cache-action@v6 - - name: Setup NodeJs - uses: actions/setup-node@v4 - with: - node-version: 16.x - registry-url: https://registry.npmjs.org - - name: Publish Docs to NPM Registry - run: sbt docs/publishToNpm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Install libuv + run: sudo apt-get update && sudo apt-get install -y libuv1-dev + - name: Setup Scala + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: '17' + check-latest: true + - name: Cache Dependencies + uses: coursier/cache-action@v6 + - name: Setup NodeJs + uses: actions/setup-node@v4 + with: + node-version: 16.x + registry-url: https://registry.npmjs.org + - name: Publish Docs to NPM Registry + run: sbt docs/publishToNpm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} notify-docs-release: name: Notify Docs Release runs-on: ubuntu-latest continue-on-error: false needs: - - release-docs + - release-docs if: ${{ (github.event_name == 'release') && (github.event.action == 'published') }} steps: - - name: Git Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: notify the main repo about the new release of docs package - run: | - PACKAGE_NAME=$(cat docs/package.json | grep '"name"' | awk -F'"' '{print $4}') - PACKAGE_VERSION=$(npm view $PACKAGE_NAME version) - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: token ${{ secrets.PAT_TOKEN }}"\ - https://api.github.com/repos/zio/zio/dispatches \ - -d '{ - "event_type":"update-docs", - "client_payload":{ - "package_name":"'"${PACKAGE_NAME}"'", - "package_version": "'"${PACKAGE_VERSION}"'" - } - }' + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: notify the main repo about the new release of docs package + run: | + PACKAGE_NAME=$(cat docs/package.json | grep '"name"' | awk -F'"' '{print $4}') + PACKAGE_VERSION=$(npm view $PACKAGE_NAME version) + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: token ${{ secrets.PAT_TOKEN }}"\ + https://api.github.com/repos/zio/zio/dispatches \ + -d '{ + "event_type":"update-docs", + "client_payload":{ + "package_name":"'"${PACKAGE_NAME}"'", + "package_version": "'"${PACKAGE_VERSION}"'" + } + }' diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 00000000..3b630f6a --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ +-Dsbt.io.implicit.relative.glob.conversion=allow diff --git a/README.md b/README.md index a619532d..4c0559d2 100644 --- a/README.md +++ b/README.md @@ -28,36 +28,190 @@ enablePlugins( ) ``` -## ZIO SBT Ecosystem +## ZIO SBT Shared + +`ZioSbtSharedPlugin` is a plugin that provides common settings and tasks which can be shared across multiple ZIO projects. This plugin includes the following settings: + +- `welcomeBannerEnabled`: A boolean setting that indicates whether or not to enable the welcome banner. +- `banners`: A sequence of strings representing banners that will be displayed as part of the welcome message. +- `usefulTasksAndSettings`: A sequence of tuples where each tuple contains a task or setting name and its description. +- `welcomeMessage`: A setting that generates the welcome message based on the `banners` and `usefulTasksAndSettings` settings. + +These settings can be used to customize the welcome message displayed when sbt starts. The welcome message can include useful tasks and settings that are relevant to the project. + +The plugin also provides the following tasks: + +- `allBanners`: A task that aggregates all banners defined in the project. +- `allUsefulTasksAndSettings`: A task that aggregates all `usefulTasksAndSettings` defined in the project. + +These tasks can be used to display a consolidated list of banners and useful tasks/settings in the welcome message. + +To use the ZIO SBT Shared plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-shared" % "0.4.0-alpha.28") +``` + +Then in your `build.sbt` file, you can configure the settings and tasks as needed: + +```scala +welcomeBannerEnabled := true +banners := Seq("Welcome to the ZIO project!") +usefulTasksAndSettings := Seq( + "compile" -> "Compiles the source code.", + "test" -> "Runs the tests." +) +``` + +To enabled the welcome message for your project, add the following line to the root of your `build.sbt` file: + +```scala +welcomeMessage +``` + +## ZIO SBT Project + +depends on: ZioSbtShared + +`ZioSbtProjectPlugin` is an sbt plugin that provides a set of common settings and tasks for compiling and testing ZIO projects. It is designed to simplify the process of setting up and configuring ZIO projects by providing a set of default settings that can be easily customized. + +This modules provides settings and tasks for Scala versions, Scala platforms, Java versions and assertions to check what config the project is using. + +### Quickstart SBT Project + +To use the ZIO SBT Project plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-project" % "0.4.0-alpha.28") +``` + +Then in your `build.sbt` file, enable the plugin by adding the following line: + +```scala +enablePlugins(ZioSbtProjectPlugin) +``` + +and add some config to `ThisBuild` scope (these are the default values, if you omit them they plugin defaults are used): + +```scala +inThisBuild( + List( + scala212 := ScalaVersion.scala212, + scala213 := ScalaVersion.scala213, + scala3 := ScalaVersion.scala3, + scalaVersion := scala213.value, + defaultCrossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) + ) +) +``` + +### Compiling + +| Platform | Scala 2.12 | Scala 2.13 | Scala 3 | +|-------------|------------|------------|-----------| +| JVM | `compileJVM2_12` | `compileJVM2_13` | `compileJVM3` | +| Scala.js | `compileJS2_12` | `compileJS2_13` | `compileJS3` | +| Scala Native| `compileNative2_12` | `compileNative2_13` | `compileNative3` | -ZIO SBT Ecosystem plugin is an sbt plugin that provides a set of sbt settings and tasks that are very common and useful for configuring and managing ZIO projects. It is designed help developers to quickly set up a new ZIO project with a minimal amount of effort. +### Testing -This pluging provides the following settings with default values: +| Platform | Scala 2.12 | Scala 2.13 | Scala 3 | +|-------------|------------|------------|-----------| +| JVM | `testJVM2_12` | `testJVM2_13` | `testJVM3` | +| Scala.js | `testJS2_12` | `testJS2_13` | `testJS3` | +| Scala Native| `testNative2_12` | `testNative2_13` | `testNative3` | -- scala212 -- scala213 -- scala3 +### Scala versions +The plugin provides the following settings for Scala versions. Use these settings to set your Scala versions. The default values are the latest stable versions of Scala 2.12, 2.13, and Scala 3. All of these settings are of type `String` and can be overridden by the user. -By having these settings, then we can use them in other sbt settings. For example, we can use them to define the `crossScalaVersions` setting: +- `scala212` - The default Scala 2.12 version +- `scala213` - The default Scala 2.13 version +- `scala3` - The default Scala 3 version +- `defaultCrossScalaVersions` - An optional setting to define the cross Scala versions +- `allScalaVersions` - This setting lists all Scala versions used by the project + +By having these settings, we can use them in other sbt settings. For example, we can use them to define the `defaultCrossScalaVersions` setting: ```scala -crossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) +ThisBuild / defaultCrossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) ``` -There are also some other settings that are useful for configuring the projects: +### Scala platforms + +- `allScalaPlatforms` - This setting lists all Scala platforms used by the project + +### Java versions + +The plugin provides the following settings for Java versions. Use these settings to set your Java versions. + +- `javaPlatform` - java target platform to release for, default is 11 +- `currentJDK` - current (detected) JDK version` + +## ZIO SBT Ecosystem + +ZIO SBT Ecosystem plugin is an sbt plugin that provides a set of sbt settings and tasks that are very common and useful for configuring and managing ZIO projects. It is designed to help developers quickly set up a new ZIO project with minimal effort. + +### Quickstart SBT Ecosystem + +depends on: ZioSbtProjectPlugin + +To use the ZIO SBT Ecosystem plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % "0.4.0-alpha.28") +``` + +Then in your `build.sbt` file, enable the plugin by adding the following line: + +```scala +enablePlugins(ZioSbtEcosystemPlugin) +``` + +use the predefined settings to configure your project: + +```scala +lazy val zio-awesome = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .in(file("zio-something")) + .settings(stdSettings(enableCrossProject = true)) + .settings(enableZIO) + .jsSettings(jsSettings) + .jvmSettings(jvmSettings) + .nativeSettings(nativeSettings) +``` + +or for an sbt-projectmatrix project: + +```scala +lazy val zio-awesome = projectMatrix + .crossScalaVersions("2.12.20", "2.13.15", "3.3.4") + .crossPlatforms(JSPlatform, JVMPlatform, NativePlatform) + .settings(stdSettings()) // enableCrossProject is not required, sbt-projectmatrix brings in default source directories for all scala versions and platforms + .settings(enableZIO) + .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = jvmSettings) + .jsPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = jsSettings) + .nativePlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = nativeSettings) +``` + +### Settings + +There are also some other (optional) settings that are useful for configuring the projects: - `stdSettings`— a set of standard settings which are common for every ZIO project, which includes configuring: - silencer plugin - kind projector plugin - - cross project plugin + - cross project source directories (custom source directories for different scala versions) - scalafix plugin - java target platform -- `enableZIO`- a set of ZIO related settings such as enabling zio streams and ZIO test framework. -- `jsSettings`, `nativeSettings`- common platform specific settings for Scala.js and Scala Native. +- `enableZIO`— a set of ZIO related settings such as enabling ZIO streams and ZIO test framework. +- `jvmSettings`— common platform-specific settings for JVM. +- `jsSettings` — common platform-specific settings for Scala.js. +- `nativeSettings`— common platform-specific settings for Scala Native. -It also provides some helper methods that are useful for configuring a compiler option for a specific Scala version: +### Roll your own settings + +It is still possible to configure the project as you like. The plugin provides some helper methods that are useful for configuring a compiler option for a specific Scala version: - `optionsOn` - `optionsOnExcept` @@ -79,7 +233,7 @@ And the same for adding a dependency for a specific Scala version: ZIO SBT Website is an SBT plugin that has the following tasks: -- `sbt compileDocs`— compile documentation inside `docs` directory. The compilation result will be inside `website/docs` directory. +- `sbt compileDocs`— compile documentation inside the `docs` directory. The compilation result will be inside the `website/docs` directory. - `sbt installWebsite`— creates a website for the project inside the `website` directory. - `sbt previewWebsite`— runs a local webserver that serves documentation locally on http://localhost:3000. By changing the documentation inside the `docs` directory, the website will be reloaded with new content. - `sbt publishToNpm`— publishes documentation inside the `docs` directory to the npm registry. @@ -87,15 +241,51 @@ ZIO SBT Website is an SBT plugin that has the following tasks: ## ZIO SBT CI Plugin +depends on: ZioSbtProjectPlugin + ZIO SBT CI is an sbt plugin which generates a GitHub workflow for a project, making it easier to set up continuous integration (CI) pipelines for Scala projects. With this plugin, developers can streamline their development workflow by automating the testing and deployment process, reducing manual effort and errors. The plugin is designed to work seamlessly with sbt, the popular build tool for Scala projects, and integrates smoothly with GitHub Actions, the CI/CD platform provided by GitHub. ZIO SBT CI provides a simple and efficient way to configure, manage, and run CI pipelines, helping teams to deliver high-quality software faster and with greater confidence. -ZIO SBT CI plugin generates a default GitHub workflow that includes common CI tasks such as building, testing, and publishing artifacts. However, users can also manually customize the workflow. This plugin is designed to be flexible and extensible, making it easy for users to tailor the workflow to their specific needs. Additionally, the plugin also provides tons of optional sbt settings that users can modify to change various aspects of the generated workflow. Overall, ZIO SBT CI plugin strikes a balance between automation and flexibility, allowing users to automate their CI process while still giving them control over how the workflow is generated. +ZIO SBT CI plugin generates a default GitHub workflow that includes common CI tasks such as building, testing, and publishing artifacts. However, users can also manually customize the workflow. This plugin is designed to be flexible and extensible, making it easy for users to tailor the workflow to their specific needs. Additionally, the plugin also provides numerous optional sbt settings that users can modify to change various aspects of the generated workflow. Overall, the ZIO SBT CI plugin strikes a balance between automation and flexibility, allowing users to automate their CI process while still giving them control over how the workflow is generated. + +### Settings + +The ZIO SBT CI plugin provides the following settings: + +- `ciTargetScalaVersions` - A mapping of project names to the Scala versions that should be used for the testing phase of continuous integration (CI). This setting is used to define the Scala versions that should be tested for each project in the build. +- `ciDefaultJavaVersion` - The default Java version to use in the CI workflow. The default value is `17`. +- `ciDefaultJavaDistribution` - The default Java distribution to use in the CI workflow. The default value is `corretto`. +- `ciDefaultNodeJSVersion` - The default Node.js version to use in the CI workflow. The default value is `16.x`. +- `ciJvmOptions` - The JVM options to use in the CI workflow. +- `ciNodeOptions` - The Node.js options to use in the CI workflow. + +### Tasks + +The ZIO SBT CI plugin provides the following tasks (steps can be overridden or modified): + +- `ciCheckGithubWorkflow` - Checks if the GitHub workflow file exists in the `.github/workflows` directory. +- `ciGenerateGithubWorkflow` - Generates a GitHub workflow file in the `.github/workflows` directory. +- `ciJobPerScalaPlatform` - Whether to generate a separate job for each Scala platform, when not defined `allScalaPlatforms.value > 1` is used +- `ciJobPerScalaVersion` - Whether to generate a separate job for each Scala version, when not defined `allScalaVersions.value > 1` is used +- `ciCheckGithubWorkflowSteps` - Steps for the GitHub workflow, for no steps use `Nil`. +- `ciCheckArtifactsCompilationSteps` - Compilation steps for the artifacts job in the GitHub workflow. +- `ciCheckArtifactsBuildSteps` - Build steps for the artifacts job in the GitHub workflow. +- `ciCheckWebsiteBuildProcess` - Build process for the website job in the GitHub workflow. +- `ciPullRequestApprovalJobs` - Jobs for the pull request approval workflow. +- `ciReleaseApprovalJobs` - Jobs for the release approval workflow. +- `ciWorkflowName` - The name of the GitHub workflow file. +- `ciEnabledBranches` - The pull request target branches on which the GitHub workflow should run. +- `ciBuildJobs` - The build jobs for the GitHub workflow. +- `ciLintJobs` - The lint jobs for the GitHub workflow. +- `ciTestJobs` - The test jobs for the GitHub workflow. +- `ciUpdateReadmeJobs` - The update readme jobs for the GitHub workflow. +- `ciReleaseJobs` - The release jobs for the GitHub workflow. +- `ciPostReleaseJobs` - The post-release jobs for the GitHub workflow. ### Getting Started -To use ZIO SBT CI plugin, add the following lines to your `plugins.sbt` file: +To use the ZIO SBT CI plugin, add the following lines to your `plugins.sbt` file: ```scala addSbtPlugin("dev.zio" % "zio-sbt-ci" % "0.4.0-alpha.28") @@ -109,13 +299,13 @@ Then in your `build.sbt` file, enable the plugin by adding the following line: enablePlugins(ZioSbtCiPlugin) ``` -Now you can generate a Github workflow by running the following command: +Now you can generate a GitHub workflow by running the following command: ```bash sbt ciGenerateGithubWorkflow ``` -This will generate a GitHub workflow file inside the `.github/workflows` directory, named `ci.yml`. The workflow file contains following default Jobs: +This will generate a GitHub workflow file inside the `.github/workflows` directory, named `ci.yml`. The workflow file contains the following default jobs: - Build - Lint @@ -130,7 +320,7 @@ This will generate a GitHub workflow file inside the `.github/workflows` directo ### Default Testing Strategy -The default testing strategy for ZIO SBT CI plugin is to run `sbt +test` on Corretto Java 11, 17 and 21. So this will generate the following job: +The default testing strategy for the ZIO SBT CI plugin is to run `sbt +test` on Corretto Java 11, 17, and 21. This will generate the following job: ```yaml test: @@ -166,20 +356,20 @@ The `sbt +test` command will run the `test` task for all submodules in the proje In some cases, we may have multiple submodules in our project and we want to test them concurrently using GitHub Actions matrix strategy. -The `ciTargetScalaVersions` setting key is used to define a mapping of project names to the Scala versions that should be used for testing phase of continuous integration (CI). +The `ciTargetScalaVersions` setting key is used to define a mapping of project names to the Scala versions that should be used for the testing phase of continuous integration (CI). -For example, suppose we have a project with the name "submoduleA" and we want to test it against Scala `2.12.19`, and for the "submoduleB" we want to test it against Scala `2.12.19` and `2.13.13` and `3.3.3`, We can define the `ciTargetScalaVersions` setting as follows: +For example, suppose we have a project with the name "submoduleA" and we want to test it against Scala `2.12.20`, and for "submoduleB" we want to test it against Scala `2.12.20`, `2.13.15`, and `3.3.4`. We can define the `ciTargetScalaVersions` setting as follows: ```scala ThisBuild / ciTargetScalaVersions := Map( - "submoduleA" -> Seq("2.12.19"), - "submoduleB" -> Seq("2.12.19", "2.13.13", "3.3.3") + "submoduleA" -> Seq("2.12.20"), + "submoduleB" -> Seq("2.12.20", "2.13.15", "3.3.4") ) ``` In the example provided, `ciTargetScalaVersions` is defined at the `ThisBuild` level, meaning that the setting will apply to all projects within the build. The setting defines a Map where the key is the name of the current project, obtained by calling the `id` method on the `thisProject` setting, and the value is a sequence of Scala versions obtained from the `crossScalaVersions` of each submodule setting. -To simplify this process, we can populate the versions using each submodule's crossScalaVersions setting as follows: +To simplify this process, we can populate the versions using each submodule's `crossScalaVersions` setting as follows: ```scala ThisBuild / ciTargetScalaVersions := Map( @@ -188,7 +378,7 @@ ThisBuild / ciTargetScalaVersions := Map( ) ``` -The above code can be simplified further by using `targetScalaVersionsFor` helper method, it takes a list of submodules and returns a Map of project names to their `crossScalaVersions`: +The above code can be simplified further by using the `targetScalaVersionsFor` helper method, which takes a list of submodules and returns a Map of project names to their `crossScalaVersions`: ```scala ThisBuild / ciTargetScalaVersions := targetScalaVersionsFor(submoduleA, submoduleB).value @@ -206,10 +396,10 @@ test: matrix: java: ['11', '17', '21'] scala-project: - - ++2.12.19 submoduleA - - ++2.12.19 submoduleB - - ++2.13.13 submoduleB - - ++3.3.3 submoduleB + - ++2.12.20 submoduleA + - ++2.12.20 submoduleB + - ++2.13.15 submoduleB + - ++3.3.4 submoduleB steps: - name: Install libuv run: sudo apt-get update && sudo apt-get install -y libuv1-dev diff --git a/build.sbt b/build.sbt index a941f5ea..2b55e4f8 100644 --- a/build.sbt +++ b/build.sbt @@ -9,13 +9,14 @@ addCommandAlias("test", "scripted") inThisBuild( List( - name := "ZIO SBT", - startYear := Some(2022), - scalaVersion := Scala212, - crossScalaVersions := Seq(scalaVersion.value), + name := "ZIO SBT", + startYear := Some(2022), + scalaVersion := Scala212, + defaultCrossScalaVersions := Seq(Scala212), developers := List( Developer("khajavi", "Milad Khajavi", "khajavi@gmail.com", url("https://github.com/khajavi")) ), + checkMima / skip := true, ciEnabledBranches := Seq("main") ) ) @@ -27,13 +28,27 @@ lazy val root = project publish / skip := true ) .aggregate( + `zio-sbt-shared`, `zio-sbt-githubactions`, `zio-sbt-website`, + `zio-sbt-project`, `zio-sbt-ecosystem`, `zio-sbt-ci`, `zio-sbt-tests` ) - .enablePlugins(ZioSbtCiPlugin) + +lazy val `zio-sbt-shared` = + project + .settings(stdSettings()) + .settings( + headerEndYear := Some(2024), + scriptedLaunchOpts := { + scriptedLaunchOpts.value ++ + Seq("-Xmx1024M", "-Dplugin.version=" + version.value) + }, + scriptedBufferLog := false + ) + .enablePlugins(SbtPlugin) lazy val `zio-sbt-tests` = project @@ -56,6 +71,20 @@ lazy val `zio-sbt-website` = ) .enablePlugins(SbtPlugin) +lazy val `zio-sbt-project` = + project + .settings(stdSettings()) + .settings( + headerEndYear := Some(2024), + scriptedLaunchOpts := { + scriptedLaunchOpts.value ++ + Seq("-Xmx1024M", "-Dplugin.version=" + version.value) + }, + scriptedBufferLog := false + ) + .enablePlugins(SbtPlugin) + .dependsOn(`zio-sbt-shared`) + lazy val `zio-sbt-ecosystem` = project .settings(stdSettings()) @@ -68,6 +97,7 @@ lazy val `zio-sbt-ecosystem` = scriptedBufferLog := false ) .enablePlugins(SbtPlugin) + .dependsOn(`zio-sbt-project`) lazy val `zio-sbt-ci` = project @@ -81,7 +111,7 @@ lazy val `zio-sbt-ci` = scriptedBufferLog := false ) .enablePlugins(SbtPlugin) - .dependsOn(`zio-sbt-githubactions`) + .dependsOn(`zio-sbt-githubactions`, `zio-sbt-project`) lazy val `zio-sbt-githubactions` = project diff --git a/docs/index.md b/docs/index.md index 31d06d14..d25ab37a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,36 +27,190 @@ enablePlugins( ) ``` -## ZIO SBT Ecosystem +## ZIO SBT Shared + +`ZioSbtSharedPlugin` is a plugin that provides common settings and tasks which can be shared across multiple ZIO projects. This plugin includes the following settings: + +- `welcomeBannerEnabled`: A boolean setting that indicates whether or not to enable the welcome banner. +- `banners`: A sequence of strings representing banners that will be displayed as part of the welcome message. +- `usefulTasksAndSettings`: A sequence of tuples where each tuple contains a task or setting name and its description. +- `welcomeMessage`: A setting that generates the welcome message based on the `banners` and `usefulTasksAndSettings` settings. + +These settings can be used to customize the welcome message displayed when sbt starts. The welcome message can include useful tasks and settings that are relevant to the project. + +The plugin also provides the following tasks: + +- `allBanners`: A task that aggregates all banners defined in the project. +- `allUsefulTasksAndSettings`: A task that aggregates all `usefulTasksAndSettings` defined in the project. + +These tasks can be used to display a consolidated list of banners and useful tasks/settings in the welcome message. + +To use the ZIO SBT Shared plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-shared" % "@VERSION@") +``` + +Then in your `build.sbt` file, you can configure the settings and tasks as needed: + +```scala +welcomeBannerEnabled := true +banners := Seq("Welcome to the ZIO project!") +usefulTasksAndSettings := Seq( + "compile" -> "Compiles the source code.", + "test" -> "Runs the tests." +) +``` + +To enabled the welcome message for your project, add the following line to the root of your `build.sbt` file: + +```scala +welcomeMessage +``` + +## ZIO SBT Project + +depends on: ZioSbtShared + +`ZioSbtProjectPlugin` is an sbt plugin that provides a set of common settings and tasks for compiling and testing ZIO projects. It is designed to simplify the process of setting up and configuring ZIO projects by providing a set of default settings that can be easily customized. + +This modules provides settings and tasks for Scala versions, Scala platforms, Java versions and assertions to check what config the project is using. + +### Quickstart SBT Project + +To use the ZIO SBT Project plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-project" % "@VERSION@") +``` + +Then in your `build.sbt` file, enable the plugin by adding the following line: + +```scala +enablePlugins(ZioSbtProjectPlugin) +``` + +and add some config to `ThisBuild` scope (these are the default values, if you omit them they plugin defaults are used): + +```scala +inThisBuild( + List( + scala212 := ScalaVersion.scala212, + scala213 := ScalaVersion.scala213, + scala3 := ScalaVersion.scala3, + scalaVersion := scala213.value, + defaultCrossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) + ) +) +``` + +### Compiling + +| Platform | Scala 2.12 | Scala 2.13 | Scala 3 | +|-------------|------------|------------|-----------| +| JVM | `compileJVM2_12` | `compileJVM2_13` | `compileJVM3` | +| Scala.js | `compileJS2_12` | `compileJS2_13` | `compileJS3` | +| Scala Native| `compileNative2_12` | `compileNative2_13` | `compileNative3` | -ZIO SBT Ecosystem plugin is an sbt plugin that provides a set of sbt settings and tasks that are very common and useful for configuring and managing ZIO projects. It is designed help developers to quickly set up a new ZIO project with a minimal amount of effort. +### Testing -This pluging provides the following settings with default values: +| Platform | Scala 2.12 | Scala 2.13 | Scala 3 | +|-------------|------------|------------|-----------| +| JVM | `testJVM2_12` | `testJVM2_13` | `testJVM3` | +| Scala.js | `testJS2_12` | `testJS2_13` | `testJS3` | +| Scala Native| `testNative2_12` | `testNative2_13` | `testNative3` | -- scala212 -- scala213 -- scala3 +### Scala versions +The plugin provides the following settings for Scala versions. Use these settings to set your Scala versions. The default values are the latest stable versions of Scala 2.12, 2.13, and Scala 3. All of these settings are of type `String` and can be overridden by the user. -By having these settings, then we can use them in other sbt settings. For example, we can use them to define the `crossScalaVersions` setting: +- `scala212` - The default Scala 2.12 version +- `scala213` - The default Scala 2.13 version +- `scala3` - The default Scala 3 version +- `defaultCrossScalaVersions` - An optional setting to define the cross Scala versions +- `allScalaVersions` - This setting lists all Scala versions used by the project + +By having these settings, we can use them in other sbt settings. For example, we can use them to define the `defaultCrossScalaVersions` setting: ```scala -crossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) +ThisBuild / defaultCrossScalaVersions := Seq(scala212.value, scala213.value, scala3.value) ``` -There are also some other settings that are useful for configuring the projects: +### Scala platforms + +- `allScalaPlatforms` - This setting lists all Scala platforms used by the project + +### Java versions + +The plugin provides the following settings for Java versions. Use these settings to set your Java versions. + +- `javaPlatform` - java target platform to release for, default is 11 +- `currentJDK` - current (detected) JDK version` + +## ZIO SBT Ecosystem + +ZIO SBT Ecosystem plugin is an sbt plugin that provides a set of sbt settings and tasks that are very common and useful for configuring and managing ZIO projects. It is designed to help developers quickly set up a new ZIO project with minimal effort. + +### Quickstart SBT Ecosystem + +depends on: ZioSbtProjectPlugin + +To use the ZIO SBT Ecosystem plugin, add the following lines to your `plugins.sbt` file: + +```scala +addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % "@VERSION@") +``` + +Then in your `build.sbt` file, enable the plugin by adding the following line: + +```scala +enablePlugins(ZioSbtEcosystemPlugin) +``` + +use the predefined settings to configure your project: + +```scala +lazy val zio-awesome = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .in(file("zio-something")) + .settings(stdSettings(enableCrossProject = true)) + .settings(enableZIO) + .jsSettings(jsSettings) + .jvmSettings(jvmSettings) + .nativeSettings(nativeSettings) +``` + +or for an sbt-projectmatrix project: + +```scala +lazy val zio-awesome = projectMatrix + .crossScalaVersions("2.12.20", "2.13.15", "3.3.4") + .crossPlatforms(JSPlatform, JVMPlatform, NativePlatform) + .settings(stdSettings()) // enableCrossProject is not required, sbt-projectmatrix brings in default source directories for all scala versions and platforms + .settings(enableZIO) + .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = jvmSettings) + .jsPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = jsSettings) + .nativePlatform(scalaVersions = Seq(Scala212, Scala213, Scala3), settings = nativeSettings) +``` + +### Settings + +There are also some other (optional) settings that are useful for configuring the projects: - `stdSettings`— a set of standard settings which are common for every ZIO project, which includes configuring: - silencer plugin - kind projector plugin - - cross project plugin + - cross project source directories (custom source directories for different scala versions) - scalafix plugin - java target platform -- `enableZIO`- a set of ZIO related settings such as enabling zio streams and ZIO test framework. -- `jsSettings`, `nativeSettings`- common platform specific settings for Scala.js and Scala Native. +- `enableZIO`— a set of ZIO related settings such as enabling ZIO streams and ZIO test framework. +- `jvmSettings`— common platform-specific settings for JVM. +- `jsSettings` — common platform-specific settings for Scala.js. +- `nativeSettings`— common platform-specific settings for Scala Native. -It also provides some helper methods that are useful for configuring a compiler option for a specific Scala version: +### Roll your own settings + +It is still possible to configure the project as you like. The plugin provides some helper methods that are useful for configuring a compiler option for a specific Scala version: - `optionsOn` - `optionsOnExcept` @@ -78,7 +232,7 @@ And the same for adding a dependency for a specific Scala version: ZIO SBT Website is an SBT plugin that has the following tasks: -- `sbt compileDocs`— compile documentation inside `docs` directory. The compilation result will be inside `website/docs` directory. +- `sbt compileDocs`— compile documentation inside the `docs` directory. The compilation result will be inside the `website/docs` directory. - `sbt installWebsite`— creates a website for the project inside the `website` directory. - `sbt previewWebsite`— runs a local webserver that serves documentation locally on http://localhost:3000. By changing the documentation inside the `docs` directory, the website will be reloaded with new content. - `sbt publishToNpm`— publishes documentation inside the `docs` directory to the npm registry. @@ -86,15 +240,51 @@ ZIO SBT Website is an SBT plugin that has the following tasks: ## ZIO SBT CI Plugin +depends on: ZioSbtProjectPlugin + ZIO SBT CI is an sbt plugin which generates a GitHub workflow for a project, making it easier to set up continuous integration (CI) pipelines for Scala projects. With this plugin, developers can streamline their development workflow by automating the testing and deployment process, reducing manual effort and errors. The plugin is designed to work seamlessly with sbt, the popular build tool for Scala projects, and integrates smoothly with GitHub Actions, the CI/CD platform provided by GitHub. ZIO SBT CI provides a simple and efficient way to configure, manage, and run CI pipelines, helping teams to deliver high-quality software faster and with greater confidence. -ZIO SBT CI plugin generates a default GitHub workflow that includes common CI tasks such as building, testing, and publishing artifacts. However, users can also manually customize the workflow. This plugin is designed to be flexible and extensible, making it easy for users to tailor the workflow to their specific needs. Additionally, the plugin also provides tons of optional sbt settings that users can modify to change various aspects of the generated workflow. Overall, ZIO SBT CI plugin strikes a balance between automation and flexibility, allowing users to automate their CI process while still giving them control over how the workflow is generated. +ZIO SBT CI plugin generates a default GitHub workflow that includes common CI tasks such as building, testing, and publishing artifacts. However, users can also manually customize the workflow. This plugin is designed to be flexible and extensible, making it easy for users to tailor the workflow to their specific needs. Additionally, the plugin also provides numerous optional sbt settings that users can modify to change various aspects of the generated workflow. Overall, the ZIO SBT CI plugin strikes a balance between automation and flexibility, allowing users to automate their CI process while still giving them control over how the workflow is generated. + +### Settings + +The ZIO SBT CI plugin provides the following settings: + +- `ciTargetScalaVersions` - A mapping of project names to the Scala versions that should be used for the testing phase of continuous integration (CI). This setting is used to define the Scala versions that should be tested for each project in the build. +- `ciDefaultJavaVersion` - The default Java version to use in the CI workflow. The default value is `17`. +- `ciDefaultJavaDistribution` - The default Java distribution to use in the CI workflow. The default value is `corretto`. +- `ciDefaultNodeJSVersion` - The default Node.js version to use in the CI workflow. The default value is `16.x`. +- `ciJvmOptions` - The JVM options to use in the CI workflow. +- `ciNodeOptions` - The Node.js options to use in the CI workflow. + +### Tasks + +The ZIO SBT CI plugin provides the following tasks (steps can be overridden or modified): + +- `ciCheckGithubWorkflow` - Checks if the GitHub workflow file exists in the `.github/workflows` directory. +- `ciGenerateGithubWorkflow` - Generates a GitHub workflow file in the `.github/workflows` directory. +- `ciJobPerScalaPlatform` - Whether to generate a separate job for each Scala platform, when not defined `allScalaPlatforms.value > 1` is used +- `ciJobPerScalaVersion` - Whether to generate a separate job for each Scala version, when not defined `allScalaVersions.value > 1` is used +- `ciCheckGithubWorkflowSteps` - Steps for the GitHub workflow, for no steps use `Nil`. +- `ciCheckArtifactsCompilationSteps` - Compilation steps for the artifacts job in the GitHub workflow. +- `ciCheckArtifactsBuildSteps` - Build steps for the artifacts job in the GitHub workflow. +- `ciCheckWebsiteBuildProcess` - Build process for the website job in the GitHub workflow. +- `ciPullRequestApprovalJobs` - Jobs for the pull request approval workflow. +- `ciReleaseApprovalJobs` - Jobs for the release approval workflow. +- `ciWorkflowName` - The name of the GitHub workflow file. +- `ciEnabledBranches` - The pull request target branches on which the GitHub workflow should run. +- `ciBuildJobs` - The build jobs for the GitHub workflow. +- `ciLintJobs` - The lint jobs for the GitHub workflow. +- `ciTestJobs` - The test jobs for the GitHub workflow. +- `ciUpdateReadmeJobs` - The update readme jobs for the GitHub workflow. +- `ciReleaseJobs` - The release jobs for the GitHub workflow. +- `ciPostReleaseJobs` - The post-release jobs for the GitHub workflow. ### Getting Started -To use ZIO SBT CI plugin, add the following lines to your `plugins.sbt` file: +To use the ZIO SBT CI plugin, add the following lines to your `plugins.sbt` file: ```scala addSbtPlugin("dev.zio" % "zio-sbt-ci" % "@VERSION@") @@ -108,13 +298,13 @@ Then in your `build.sbt` file, enable the plugin by adding the following line: enablePlugins(ZioSbtCiPlugin) ``` -Now you can generate a Github workflow by running the following command: +Now you can generate a GitHub workflow by running the following command: ```bash sbt ciGenerateGithubWorkflow ``` -This will generate a GitHub workflow file inside the `.github/workflows` directory, named `ci.yml`. The workflow file contains following default Jobs: +This will generate a GitHub workflow file inside the `.github/workflows` directory, named `ci.yml`. The workflow file contains the following default jobs: - Build - Lint @@ -129,7 +319,7 @@ This will generate a GitHub workflow file inside the `.github/workflows` directo ### Default Testing Strategy -The default testing strategy for ZIO SBT CI plugin is to run `sbt +test` on Corretto Java 11, 17 and 21. So this will generate the following job: +The default testing strategy for the ZIO SBT CI plugin is to run `sbt +test` on Corretto Java 11, 17, and 21. This will generate the following job: ```yaml test: @@ -165,20 +355,20 @@ The `sbt +test` command will run the `test` task for all submodules in the proje In some cases, we may have multiple submodules in our project and we want to test them concurrently using GitHub Actions matrix strategy. -The `ciTargetScalaVersions` setting key is used to define a mapping of project names to the Scala versions that should be used for testing phase of continuous integration (CI). +The `ciTargetScalaVersions` setting key is used to define a mapping of project names to the Scala versions that should be used for the testing phase of continuous integration (CI). -For example, suppose we have a project with the name "submoduleA" and we want to test it against Scala `2.12.19`, and for the "submoduleB" we want to test it against Scala `2.12.19` and `2.13.13` and `3.3.3`, We can define the `ciTargetScalaVersions` setting as follows: +For example, suppose we have a project with the name "submoduleA" and we want to test it against Scala `2.12.20`, and for "submoduleB" we want to test it against Scala `2.12.20`, `2.13.15`, and `3.3.4`. We can define the `ciTargetScalaVersions` setting as follows: ```scala ThisBuild / ciTargetScalaVersions := Map( - "submoduleA" -> Seq("2.12.19"), - "submoduleB" -> Seq("2.12.19", "2.13.13", "3.3.3") + "submoduleA" -> Seq("2.12.20"), + "submoduleB" -> Seq("2.12.20", "2.13.15", "3.3.4") ) ``` In the example provided, `ciTargetScalaVersions` is defined at the `ThisBuild` level, meaning that the setting will apply to all projects within the build. The setting defines a Map where the key is the name of the current project, obtained by calling the `id` method on the `thisProject` setting, and the value is a sequence of Scala versions obtained from the `crossScalaVersions` of each submodule setting. -To simplify this process, we can populate the versions using each submodule's crossScalaVersions setting as follows: +To simplify this process, we can populate the versions using each submodule's `crossScalaVersions` setting as follows: ```scala ThisBuild / ciTargetScalaVersions := Map( @@ -187,7 +377,7 @@ ThisBuild / ciTargetScalaVersions := Map( ) ``` -The above code can be simplified further by using `targetScalaVersionsFor` helper method, it takes a list of submodules and returns a Map of project names to their `crossScalaVersions`: +The above code can be simplified further by using the `targetScalaVersionsFor` helper method, which takes a list of submodules and returns a Map of project names to their `crossScalaVersions`: ```scala ThisBuild / ciTargetScalaVersions := targetScalaVersionsFor(submoduleA, submoduleB).value @@ -205,10 +395,10 @@ test: matrix: java: ['11', '17', '21'] scala-project: - - ++2.12.19 submoduleA - - ++2.12.19 submoduleB - - ++2.13.13 submoduleB - - ++3.3.3 submoduleB + - ++2.12.20 submoduleA + - ++2.12.20 submoduleB + - ++2.13.15 submoduleB + - ++3.3.4 submoduleB steps: - name: Install libuv run: sudo apt-get update && sudo apt-get install -y libuv1-dev diff --git a/project/Versions.scala b/project/Versions.scala index 03da9412..35247076 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val Scala212 = "2.12.19" - val Scala213 = "2.13.13" - val Scala3 = "3.3.3" - val zio = "2.0.21" + val Scala212 = "2.12.20" + val Scala213 = "2.13.15" + val Scala3 = "3.3.4" + val zio = "2.1.11" } diff --git a/project/build.properties b/project/build.properties index ee4c672c..1767a6f8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.1 +sbt.version = 1.10.5 diff --git a/project/build.sbt b/project/build.sbt index 523fb5f7..e4cbfb04 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -1,3 +1,9 @@ +Compile / unmanagedSourceDirectories += file("zio-sbt-shared/src/main/scala") +Compile / unmanagedResourceDirectories += file("zio-sbt-shared/src/main/resources") + +Compile / unmanagedSourceDirectories += file("zio-sbt-project/src/main/scala") +Compile / unmanagedResourceDirectories += file("zio-sbt-project/src/main/resources") + Compile / unmanagedSourceDirectories += file("zio-sbt-ecosystem/src/main/scala") Compile / unmanagedResourceDirectories += file("zio-sbt-ecosystem/src/main/resources") diff --git a/project/plugins.sbt b/project/plugins.sbt index b9292898..082bfe5a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,30 +1,30 @@ // Build Server Plugins -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.5") // Linting Plugins addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") // Versioning and Release Plugins -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0") // Docs Plugins -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") // Cross-Compiler Plugins -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.2") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") -// Benchmarking Plugins -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") - -libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" -libraryDependencies += "dev.zio" %% "zio" % "2.1.8" -libraryDependencies += "io.circe" %% "circe-yaml" % "0.16.0" +libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.8" +libraryDependencies += "dev.zio" %% "zio" % "2.1.11" +libraryDependencies += "dev.zio" %% "zio-json" % "0.7.3" +libraryDependencies += "dev.zio" %% "zio-json-yaml" % "0.7.3" diff --git a/zio-sbt-ci/build.sbt b/zio-sbt-ci/build.sbt index b7d36a60..b8e1d71f 100644 --- a/zio-sbt-ci/build.sbt +++ b/zio-sbt-ci/build.sbt @@ -1,2 +1,3 @@ -libraryDependencies += "dev.zio" %% "zio" % "2.1.8" -libraryDependencies += "io.circe" %% "circe-yaml" % "0.16.0" +libraryDependencies += "dev.zio" %% "zio" % "2.1.11" +libraryDependencies += "dev.zio" %% "zio-json" % "0.7.3" +libraryDependencies += "dev.zio" %% "zio-json-yaml" % "0.7.3" diff --git a/zio-sbt-ci/src/main/scala/zio/sbt/CiVersions.scala b/zio-sbt-ci/src/main/scala/zio/sbt/CiVersions.scala new file mode 100644 index 00000000..2d1428c7 --- /dev/null +++ b/zio-sbt-ci/src/main/scala/zio/sbt/CiVersions.scala @@ -0,0 +1,8 @@ +package zio.sbt + +object CiVersions { + + val TargetJavaVersions: Seq[String] = Seq("11", "17", "21") + val JavaDistribution = "corretto" + val NodeJS = "16.x" +} diff --git a/zio-sbt-ci/src/main/scala/zio/sbt/V.scala b/zio-sbt-ci/src/main/scala/zio/sbt/V.scala index d123c12a..f71e1f18 100644 --- a/zio-sbt-ci/src/main/scala/zio/sbt/V.scala +++ b/zio-sbt-ci/src/main/scala/zio/sbt/V.scala @@ -5,7 +5,7 @@ object V { Map( "peter-evans/create-pull-request" -> "v6", "zio/generate-github-app-token" -> "v1.0.0", - "pierotofy/set-swap-space" -> "master", + "pierotofy/set-swap-space" -> "49819abfb41bd9b44fb781159c033dba90353a7c", // 1.0, "actions/checkout" -> "v4", "coursier/cache-action" -> "v6", "actions/setup-java" -> "v4", diff --git a/zio-sbt-ci/src/main/scala/zio/sbt/ZioSbtCiPlugin.scala b/zio-sbt-ci/src/main/scala/zio/sbt/ZioSbtCiPlugin.scala index 70371ac6..6aba56f7 100644 --- a/zio-sbt-ci/src/main/scala/zio/sbt/ZioSbtCiPlugin.scala +++ b/zio-sbt-ci/src/main/scala/zio/sbt/ZioSbtCiPlugin.scala @@ -15,24 +15,24 @@ */ package zio.sbt +import scala.collection.immutable.ListMap import scala.language.experimental.macros import scala.sys.process._ -import io.circe._ -import io.circe.syntax._ -import io.circe.yaml.Printer.{LineBreak, YamlVersion} import sbt.{Def, io => _, _} -import zio.sbt.githubactions.Step.SingleStep -import zio.sbt.githubactions.{Job, Step, _} +import zio.json._ +import zio.json.yaml._ +import zio.sbt.githubactionsnative.Step.SingleStep +import zio.sbt.githubactionsnative.{Job, Step, _} object ZioSbtCiPlugin extends AutoPlugin { - override def requires = plugins.CorePlugin - override def trigger = allRequirements + override def requires: Plugins = plugins.CorePlugin && ZioSbtCrossbuildPlugin + override def trigger = allRequirements object autoImport { val ciDocsVersioningScheme: SettingKey[DocsVersioning] = settingKey[DocsVersioning]("Docs versioning style") - val ciEnabledBranches: SettingKey[Seq[String]] = settingKey[Seq[String]]("Publish branch for documentation") + val ciEnabledBranches: SettingKey[Seq[String]] = settingKey[Seq[String]]("Branches to trigger CI on") val ciGroupSimilarTests: SettingKey[Boolean] = settingKey[Boolean]("Group similar test by their Java and Scala versions, default is false") val ciMatrixMaxParallel: SettingKey[Option[Int]] = @@ -46,6 +46,14 @@ object ZioSbtCiPlugin extends AutoPlugin { settingKey[Option[Condition]]("condition to update readme") val ciTargetJavaVersions: SettingKey[Seq[String]] = settingKey[Seq[String]]("The default target Java versions for all modules, default is 11, 17, 21") + + val ciJobPerScalaPlatform: SettingKey[Boolean] = + settingKey[Boolean]( + "Create a job for each Scala platform, when not defined `allScalaPlatforms.value > 1` is used" + ) + val ciJobPerScalaVersion: SettingKey[Boolean] = + settingKey[Boolean]("Create a job for each Scala version, when not defined `allScalaVersions.value > 1` is used") + val ciTargetMinJavaVersions: SettingKey[Map[String, String]] = SettingKey[Map[String, String]]( "minimum target Java version for each module, default is an empty map which makes CI to use `ciAllTargetJavaVersions` to determine the minimum target Java version for all modules" @@ -58,6 +66,10 @@ object ZioSbtCiPlugin extends AutoPlugin { settingKey[String]( "The default Java version which is used in CI, especially for releasing artifacts, defaults to 17. Note that this is just JDK version used for compilation. Artefact will be compiled with -target and -source flags specified by 'javaPlatform' setting or 'javaPlatform' parameter in 'stdSettings'" ) + val ciDefaultJavaDistribution: SettingKey[String] = + settingKey[String]("The default Java distribution, default is Corretto") + val ciDefaultNodeJSVersion: SettingKey[String] = + settingKey[String]("The default NodeJS version, default is 16.x") val ciCheckGithubWorkflow: TaskKey[Unit] = taskKey[Unit]("Make sure if the ci.yml file is up-to-date") val ciCheckArtifactsBuildSteps: SettingKey[Seq[Step]] = settingKey[Seq[Step]]("Workflow steps for checking artifact build process") @@ -90,18 +102,21 @@ object ZioSbtCiPlugin extends AutoPlugin { import autoImport.* + implicit class CommandOps(command: String) { + def onlyForCrossbuild(crossbuild: Boolean): String = + if (crossbuild) command else "" + } + lazy val buildJobs: Def.Initialize[Seq[Job]] = Def.setting { val swapSizeGB = ciSwapSizeGB.value val setSwapSpace = SetSwapSpace.value val checkout = Checkout.value - val javaVersion = ciDefaultJavaVersion.value val checkAllCodeCompiles = ciCheckArtifactsCompilationSteps.value val checkArtifactBuildProcess = ciCheckArtifactsBuildSteps.value val checkWebsiteBuildProcess = ciCheckWebsiteBuildProcess.value Seq( Job( - id = "build", name = "Build", continueOnError = true, steps = { @@ -109,9 +124,11 @@ object ZioSbtCiPlugin extends AutoPlugin { Seq( checkout, SetupLibuv, - SetupJava(javaVersion), + SetupJava(ciDefaultJavaVersion.value, ciDefaultJavaDistribution.value), CacheDependencies - ) ++ checkAllCodeCompiles ++ checkArtifactBuildProcess ++ checkWebsiteBuildProcess + ) ++ checkAllCodeCompiles.flatMap(_.flatten) ++ checkArtifactBuildProcess.flatMap( + _.flatten + ) ++ checkWebsiteBuildProcess.flatMap(_.flatten) } ) ) @@ -121,202 +138,254 @@ object ZioSbtCiPlugin extends AutoPlugin { val checkout = Checkout.value val swapSizeGB = ciSwapSizeGB.value val setSwapSpace = SetSwapSpace.value - val javaVersion = ciDefaultJavaVersion.value val checkGithubWorkflow = ciCheckGithubWorkflowSteps.value - val lint = Lint.value Seq( Job( - id = "lint", name = "Lint", steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ - Seq(checkout, SetupLibuv, SetupJava(javaVersion), CacheDependencies) ++ checkGithubWorkflow ++ Seq(lint) + Seq( + checkout, + SetupLibuv, + SetupJava(ciDefaultJavaVersion.value, ciDefaultJavaDistribution.value), + CacheDependencies + ) ++ checkGithubWorkflow.flatMap( + _.flatten + ) ++ + Seq(Lint.value) ) ) } - lazy val testJobs: Def.Initialize[Seq[Job]] = Def.setting { - val groupSimilarTests = ciGroupSimilarTests.value - val scalaVersionMatrix = ciTargetScalaVersions.value - val javaPlatforms = autoImport.ciTargetJavaVersions.value - val javaPlatformMatrix = ciTargetMinJavaVersions.value - val matrixMaxParallel = ciMatrixMaxParallel.value - val swapSizeGB = ciSwapSizeGB.value - val setSwapSpace = SetSwapSpace.value - val checkout = Checkout.value - val backgroundJobs = ciBackgroundJobs.value - - val prefixJobs = makePrefixJobs(backgroundJobs) - - val GroupTests = { - def makeTests(scalaVersion: String) = - s" ${scalaVersionMatrix.filter { case (_, versions) => - versions.contains(scalaVersion) - }.map(e => e._1 + "/test").mkString(" ")}" - - Job( - id = "test", - name = "Test", - strategy = Some( - Strategy( - matrix = Map( - "java" -> javaPlatforms.toList, - "scala" -> scalaVersionMatrix.values.flatten.toSet.toList - ), - maxParallel = matrixMaxParallel, - failFast = false - ) - ), - steps = { - (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ Seq( - SetupLibuv, - SetupJava("${{ matrix.java }}"), - CacheDependencies, - checkout - ) ++ (if (javaPlatformMatrix.values.toSet.isEmpty) { - scalaVersionMatrix.values.toSeq.flatten.distinct.map { scalaVersion: String => - Step.SingleStep( - name = "Test", - condition = Some(Condition.Expression(s"matrix.scala == '$scalaVersion'")), - run = Some( - prefixJobs + "sbt ++${{ matrix.scala }}" + makeTests( - scalaVersion + lazy val testJobs: Def.Initialize[Seq[Job]] = Def.settingDyn { + Def.setting { + val groupSimilarTests = ciGroupSimilarTests.value + val scalaVersionMatrix = ciTargetScalaVersions.value + val javaVersions = autoImport.ciTargetJavaVersions.value + val scalaVersions = ScalaVersions.Keys.allScalaVersions.value + val scalaPlatforms = ScalaPlatforms.Keys.allScalaPlatforms.value + val javaPlatformMatrix = ciTargetMinJavaVersions.value + val matrixMaxParallel = ciMatrixMaxParallel.value + val swapSizeGB = ciSwapSizeGB.value + val setSwapSpace = SetSwapSpace.value + val checkout = Checkout.value + val backgroundJobs = ciBackgroundJobs.value + + val prefixJobs = makePrefixJobs(backgroundJobs) + + val isSingleBuild = IsSingleBuild.value + val crossbuildOrNot = CrossbuildOrNot.value + + val jobPerScalaPlatform = ciJobPerScalaPlatform.?.value.getOrElse(scalaPlatforms.size > 1) + val jobPerScalaVersion = ciJobPerScalaVersion.?.value.getOrElse(scalaVersions.size > 1) + + def setScalaVersionOrNot(scalaVersion: String) = if (isSingleBuild) "" else s"++$scalaVersion" + + val testTask: String => String = + if (isSingleBuild) + (scalaVersion: String) => "test" + TestTasks.scalaVersionToSuffix(scalaVersion) + else + _ => "test" + + val GroupTests = { + def makeTests(scalaVersion: String) = + s"${scalaVersionMatrix.filter { case (_, versions) => + versions.contains(scalaVersion) + }.map(e => e._1 + "/" + testTask(scalaVersion)).mkString(" ")}" + + Job( + name = "Test", + strategy = Some( + Strategy( + matrix = ListMap( + "java" -> javaVersions.toList.sorted, + "scala" -> scalaVersionMatrix.values.flatten.toList.distinct.sorted + ), + maxParallel = matrixMaxParallel, + failFast = false + ) + ), + steps = { + (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ Seq( + SetupLibuv, + SetupJava("${{ matrix.java }}", ciDefaultJavaDistribution.value), + CacheDependencies, + checkout + ) ++ (if (javaPlatformMatrix.values.toSet.isEmpty) { + scalaVersionMatrix.values.toSeq.flatten.distinct.map { scalaVersion: String => + Step.SingleStep( + name = "Test", + `if` = Some(Condition.Expression(s"matrix.scala == '$scalaVersion'")), + run = Some( + Seq(prefixJobs, "sbt", setScalaVersionOrNot("${{ matrix.scala }}"), makeTests(scalaVersion)) + .filterNot(_.isBlank()) + .mkString(" ") ) ) - ) - } - } else { - (for { - javaPlatform: String <- Set("11", "17", "21") - scalaVersion: String <- scalaVersionMatrix.values.toSeq.flatten.toSet - projects = - scalaVersionMatrix.filterKeys { p => - javaPlatformMatrix.getOrElse(p, javaPlatform).toInt <= javaPlatform.toInt - }.filter { case (_, versions) => - versions.contains(scalaVersion) - }.keys - } yield - if (projects.nonEmpty) - Seq( - Step.SingleStep( - name = "Test", - condition = Some( - Condition.Expression(s"matrix.java == '$javaPlatform'") && Condition.Expression( - s"matrix.scala == '$scalaVersion'" + } + } else { + (for { + javaPlatform: String <- Set("11", "17", "21") + scalaVersion: String <- scalaVersionMatrix.values.toSeq.flatten.toSet + projects = + scalaVersionMatrix.filterKeys { p => + javaPlatformMatrix.getOrElse(p, javaPlatform).toInt <= javaPlatform.toInt + }.filter { case (_, versions) => + versions.contains(scalaVersion) + }.keys + } yield + if (projects.nonEmpty) + Seq( + Step.SingleStep( + name = "Test", + `if` = Some( + Condition.Expression(s"matrix.java == '$javaPlatform'") && Condition.Expression( + s"matrix.scala == '$scalaVersion'" + ) + ), + run = Some( + Seq( + prefixJobs, + "sbt", + setScalaVersionOrNot("${{ matrix.scala }}"), + projects.map(_ + "/" + testTask(scalaVersion)).mkString(" ") + ).filterNot(_.isBlank()).mkString(" ") ) - ), - run = Some( - prefixJobs + "sbt ++${{ matrix.scala }}" ++ s" ${projects.map(_ + "/test ").mkString(" ")}" ) ) - ) - else Seq.empty).flatten.toSeq - }) - } - ) - } + else Seq.empty).flatten.toSeq + }) + } + ) + } - val FlattenTests = - Job( - id = "test", - name = "Test", - strategy = Some( - Strategy( - matrix = Map( - "java" -> javaPlatforms.toList - ) ++ - (if (javaPlatformMatrix.isEmpty) { - Map("scala-project" -> scalaVersionMatrix.flatMap { case (moduleName, versions) => - versions.map { version => - s"++$version $moduleName" - } - }.toList) - } else { - def generateScalaProjectJavaPlatform(javaPlatform: String) = - s"scala-project-java$javaPlatform" -> scalaVersionMatrix.filterKeys { p => - javaPlatformMatrix.getOrElse(p, javaPlatform).toInt <= javaPlatform.toInt - }.flatMap { case (moduleName, versions) => + val FlattenTests = + Job( + name = "Test", + strategy = Some( + Strategy( + matrix = ListMap( + "java" -> javaVersions.toList.sorted + ) ++ + (if (javaPlatformMatrix.isEmpty) { + ListMap("scala-project" -> scalaVersionMatrix.flatMap { case (moduleName, versions) => versions.map { version => - s"++$version $moduleName" + Seq(setScalaVersionOrNot(version), moduleName + "/" + testTask(version)) + .filterNot(_.isBlank()) + .mkString(" ") } - }.toList - - javaPlatforms.map(jp => generateScalaProjectJavaPlatform(jp)) - }), - maxParallel = matrixMaxParallel, - failFast = false - ) - ), - steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ - Seq( - SetupLibuv, - SetupJava("${{ matrix.java }}"), - CacheDependencies, - checkout, - if (javaPlatformMatrix.values.toSet.isEmpty) { - Step.SingleStep( - name = "Test", - run = Some(prefixJobs + "sbt ${{ matrix.scala-project }}/test") - ) - } else { - Step.StepSequence( + }.toList) + } else { + def generateScalaProjectJavaPlatform(javaPlatform: String) = + s"scala-project-java$javaPlatform" -> scalaVersionMatrix.filterKeys { p => + javaPlatformMatrix.getOrElse(p, javaPlatform).toInt <= javaPlatform.toInt + }.flatMap { case (moduleName, versions) => + versions.map { version => + Seq(setScalaVersionOrNot(version), moduleName + "/" + testTask(version)) + .filterNot(_.isBlank()) + .mkString(" ") + } + }.toList + + javaVersions.map(jp => generateScalaProjectJavaPlatform(jp)) + }), + maxParallel = matrixMaxParallel, + failFast = false + ) + ), + steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ + Seq( + SetupLibuv, + SetupJava("${{ matrix.java }}", ciDefaultJavaDistribution.value), + CacheDependencies, + checkout + ) ++ ( + if (javaPlatformMatrix.values.toSet.isEmpty) { + Seq( + Step.SingleStep( + name = "Test", + run = Some(prefixJobs + "sbt ${{ matrix.scala-project }}") + ) + ) + } else { Seq( Step.SingleStep( name = "Java 11 Tests", - condition = Some(Condition.Expression("matrix.java == '11'")), + `if` = Some(Condition.Expression("matrix.java == '11'")), run = Some( - prefixJobs + "sbt ${{ matrix.scala-project-java11 }}/test" + prefixJobs + "sbt ${{ matrix.scala-project-java11 }}" ) ), Step.SingleStep( name = "Java 17 Tests", - condition = Some(Condition.Expression("matrix.java == '17'")), + `if` = Some(Condition.Expression("matrix.java == '17'")), run = Some( - prefixJobs + "sbt ${{ matrix.scala-project-java17 }}/test" + prefixJobs + "sbt ${{ matrix.scala-project-java17 }}" ) ), Step.SingleStep( name = "Java 21 Tests", - condition = Some(Condition.Expression("matrix.java == '21'")), + `if` = Some(Condition.Expression("matrix.java == '21'")), run = Some( - prefixJobs + "sbt ${{ matrix.scala-project-java21 }}/test" + prefixJobs + "sbt ${{ matrix.scala-project-java21 }}" ) ) ) - ) - - } - ) - ) + } + ) + ) - val DefaultTestStrategy = - Job( - id = "test", - name = "Test", - strategy = Some( - Strategy( - matrix = Map("java" -> javaPlatforms.toList), - maxParallel = matrixMaxParallel, - failFast = false - ) - ), - steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ - Seq( - SetupLibuv, - SetupJava("${{ matrix.java }}"), - CacheDependencies, - checkout, - Step.SingleStep( - name = "Test", - run = Some(prefixJobs + "sbt +test") + val DefaultTestStrategy = { + val ((testMatrix: ListMap[String, List[String]]), (testTaskSuffix: String)) = + if (jobPerScalaPlatform && jobPerScalaVersion) { + ListMap( + "scalaVersion" -> scalaVersions.toList.sorted, + "scalaPlatform" -> scalaPlatforms.toList.map(_.asString).sorted + ) -> + "${{ matrix.scalaPlatform }}${{ startsWith(matrix.scalaVersion, '2.12') && '2_12' || (startsWith(matrix.scalaVersion, '2.13') && '2_13' || (startsWith(matrix.scalaVersion, '3') && '3' || '')) }}" + } else if (jobPerScalaPlatform) { + ListMap( + "scalaPlatform" -> scalaPlatforms.toList.map(_.asString).sorted + ) -> "${{ matrix.scalaPlatform }}" + } else if (jobPerScalaVersion) { + ListMap( + "scalaVersion" -> scalaVersions.toList.sorted + ) -> "${{ startsWith(matrix.scalaVersion, '2.12') && '2_12' || (startsWith(matrix.scalaVersion, '2.13') && '2_13' || (startsWith(matrix.scalaVersion, '3') && '3' || '')) }}" + } else { + ListMap.empty -> "" + } + Job( + name = "Test", + strategy = Some( + Strategy( + matrix = ListMap( + "java" -> javaVersions.toList.sorted + ) ++ testMatrix, + maxParallel = matrixMaxParallel, + failFast = false ) - ) - ) + ), + steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ + Seq( + SetupLibuv, + SetupJava("${{ matrix.java }}", ciDefaultJavaDistribution.value), + CacheDependencies, + checkout, + Step.SingleStep( + name = "Test", + run = Some( + prefixJobs + s"sbt ${crossbuildOrNot}test" ++ testTaskSuffix + ) + ) + ) + ) + } - if (javaPlatformMatrix.isEmpty && scalaVersionMatrix.isEmpty) - Seq(DefaultTestStrategy) - else - Seq(if (groupSimilarTests) GroupTests else FlattenTests) + if (javaPlatformMatrix.isEmpty && scalaVersionMatrix.isEmpty) + Seq(DefaultTestStrategy) + else + Seq(if (groupSimilarTests) GroupTests else FlattenTests) + } } lazy val reportSuccessfulJobs: Def.Initialize[Seq[Job]] = Def.setting { @@ -324,9 +393,8 @@ object ZioSbtCiPlugin extends AutoPlugin { Seq( Job( - id = "ci", name = "ci", - need = pullRequestApprovalJobs, + needs = Some(pullRequestApprovalJobs), steps = Seq( SingleStep( name = "Report Successful CI", @@ -341,20 +409,18 @@ object ZioSbtCiPlugin extends AutoPlugin { val swapSizeGB = ciSwapSizeGB.value val setSwapSpace = SetSwapSpace.value val checkout = Checkout.value - val javaVersion = ciDefaultJavaVersion.value val updateReadmeCondition = autoImport.ciUpdateReadmeCondition.value val generateReadme = GenerateReadme.value Seq( Job( - id = "update-readme", name = "Update README", - condition = updateReadmeCondition orElse Some(Condition.Expression("github.event_name == 'push'")), + `if` = updateReadmeCondition orElse Some(Condition.Expression("github.event_name == 'push'")), steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ Seq( checkout, SetupLibuv, - SetupJava(javaVersion), + SetupJava(ciDefaultJavaVersion.value, ciDefaultJavaDistribution.value), CacheDependencies, generateReadme, Step.SingleStep( @@ -369,44 +435,52 @@ object ZioSbtCiPlugin extends AutoPlugin { name = "Generate Token", id = Some("generate-token"), uses = Some(ActionRef(V("zio/generate-github-app-token"))), - parameters = Map( - "app_id" -> "${{ secrets.APP_ID }}".asJson, - "app_private_key" -> "${{ secrets.APP_PRIVATE_KEY }}".asJson + `with` = Some( + Map( + "app_id" -> "${{ secrets.APP_ID }}".toJsonAST.right.get, + "app_private_key" -> "${{ secrets.APP_PRIVATE_KEY }}".toJsonAST.right.get + ) ) ), Step.SingleStep( name = "Create Pull Request", id = Some("cpr"), uses = Some(ActionRef(V("peter-evans/create-pull-request"))), - parameters = Map( - "title" -> "Update README.md".asJson, - "commit-message" -> "Update README.md".asJson, - "branch" -> "zio-sbt-website/update-readme".asJson, - "delete-branch" -> true.asJson, - "body" -> - """|Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. - | - |I will automatically update the README.md file whenever there is new change for README.md, e.g. - | - After each release, I will update the version in the installation section. - | - After any changes to the "docs/index.md" file, I will update the README.md file accordingly.""".stripMargin.asJson, - "token" -> "${{ steps.generate-token.outputs.token }}".asJson + `with` = Some( + Map( + "title" -> "Update README.md".toJsonAST.right.get, + "commit-message" -> "Update README.md".toJsonAST.right.get, + "branch" -> "zio-sbt-website/update-readme".toJsonAST.right.get, + "delete-branch" -> true.toJsonAST.right.get, + "body" -> + """|Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. + | + |I will automatically update the README.md file whenever there is new change for README.md, e.g. + | - After each release, I will update the version in the installation section. + | - After any changes to the "docs/index.md" file, I will update the README.md file accordingly.""".stripMargin.toJsonAST.right.get, + "token" -> "${{ steps.generate-token.outputs.token }}".toJsonAST.right.get + ) ) ), Step.SingleStep( name = "Approve PR", - condition = Some(Condition.Expression("steps.cpr.outputs.pull-request-number")), - env = Map( - "GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}", - "PR_URL" -> "${{ steps.cpr.outputs.pull-request-url }}" + `if` = Some(Condition.Expression("steps.cpr.outputs.pull-request-number")), + env = Some( + ListMap( + "GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}", + "PR_URL" -> "${{ steps.cpr.outputs.pull-request-url }}" + ) ), run = Some("gh pr review \"$PR_URL\" --approve") ), Step.SingleStep( name = "Enable Auto-Merge", - condition = Some(Condition.Expression("steps.cpr.outputs.pull-request-number")), - env = Map( - "GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}", - "PR_URL" -> "${{ steps.cpr.outputs.pull-request-url }}" + `if` = Some(Condition.Expression("steps.cpr.outputs.pull-request-number")), + env = Some( + ListMap( + "GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}", + "PR_URL" -> "${{ steps.cpr.outputs.pull-request-url }}" + ) ), run = Some("gh pr merge --auto --squash \"$PR_URL\" || gh pr merge --squash \"$PR_URL\"") ) @@ -419,21 +493,19 @@ object ZioSbtCiPlugin extends AutoPlugin { val swapSizeGB = ciSwapSizeGB.value val setSwapSpace = SetSwapSpace.value val checkout = Checkout.value - val javaVersion = ciDefaultJavaVersion.value val release = Release.value val jobs = ciReleaseApprovalJobs.value Seq( Job( - id = "release", name = "Release", - need = jobs, - condition = Some(Condition.Expression("github.event_name != 'pull_request'")), + needs = Some(jobs), + `if` = Some(Condition.Expression("github.event_name != 'pull_request'")), steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ Seq( checkout, SetupLibuv, - SetupJava(javaVersion), + SetupJava(ciDefaultJavaVersion.value, ciDefaultJavaDistribution.value), CacheDependencies, release ) @@ -445,15 +517,14 @@ object ZioSbtCiPlugin extends AutoPlugin { val swapSizeGB = ciSwapSizeGB.value val setSwapSpace = SetSwapSpace.value val checkout = Checkout.value - val javaVersion = ciDefaultJavaVersion.value + val nodeJsVersion = ciDefaultNodeJSVersion.value val publishToNpmRegistry = PublishToNpmRegistry.value Seq( Job( - id = "release-docs", name = "Release Docs", - need = Seq("release"), - condition = Some( + needs = Some(Seq("release")), + `if` = Some( Condition.Expression("github.event_name == 'release'") && Condition.Expression("github.event.action == 'published'") || Condition.Expression( "github.event_name == 'workflow_dispatch'" @@ -461,23 +532,18 @@ object ZioSbtCiPlugin extends AutoPlugin { ), steps = (if (swapSizeGB > 0) Seq(setSwapSpace) else Seq.empty) ++ Seq( - Step.StepSequence( - Seq( - checkout, - SetupLibuv, - SetupJava(javaVersion), - CacheDependencies, - SetupNodeJs, - publishToNpmRegistry - ) - ) + checkout, + SetupLibuv, + SetupJava(ciDefaultJavaVersion.value, ciDefaultJavaDistribution.value), + CacheDependencies, + SetupNodeJs(nodeJsVersion), + publishToNpmRegistry ) ), Job( - id = "notify-docs-release", name = "Notify Docs Release", - need = Seq("release-docs"), - condition = Some( + needs = Some(Seq("release-docs")), + `if` = Some( Condition.Expression("github.event_name == 'release'") && Condition.Expression("github.event.action == 'published'") ), @@ -520,72 +586,92 @@ object ZioSbtCiPlugin extends AutoPlugin { val jvmOptions = Seq("-XX:+PrintCommandLineFlags") ++ ciJvmOptions.value val nodeOptions = ciNodeOptions.value - val jvmMap = Map( + val jvmMap = ListMap( "JDK_JAVA_OPTIONS" -> jvmOptions.mkString(" ") ) - val nodeMap: Map[String, String] = - if (nodeOptions.nonEmpty) Map("NODE_OPTIONS" -> nodeOptions.mkString(" ")) else Map.empty - - val workflow = yaml - .Printer( - preserveOrder = true, - dropNullKeys = true, - splitLines = false, - lineBreak = LineBreak.Unix, - version = YamlVersion.Auto + val nodeMap: ListMap[String, String] = + if (nodeOptions.nonEmpty) ListMap("NODE_OPTIONS" -> nodeOptions.mkString(" ")) else ListMap.empty + + val yamlOptions = + YamlOptions.default.copy( + dropNulls = true, + lineBreak = org.yaml.snakeyaml.DumperOptions.LineBreak.UNIX, + maxScalarWidth = Some(1024) ) - .pretty( - Workflow( - name = workflowName, - env = jvmMap ++ nodeMap, - triggers = Seq( - Trigger.WorkflowDispatch(), - Trigger.Release(Seq("published")), - Trigger.Push(branches = enabledBranches.map(Branch.Named)), - Trigger.PullRequest(ignoredBranches = Seq(Branch.Named("gh-pages"))) - ), - jobs = - buildJobs ++ lintJobs ++ testJobs ++ updateReadmeJobs ++ reportSuccessful ++ releaseJobs ++ postReleaseJobs - ).asJson + + val workflow = + Workflow( + name = workflowName, + env = Some(jvmMap ++ nodeMap), + on = Some( + Triggers( + release = Some(Trigger.Release(Seq(Trigger.ReleaseType.Published))), + push = Some(Trigger.Push(branches = Some(enabledBranches.map(Branch.Named)).filter(_.nonEmpty))), + pullRequest = Some(Trigger.PullRequest(branchesIgnore = Some(Seq(Branch.Named("gh-pages"))))) + ) + ), + jobs = ListMap( + (buildJobs ++ lintJobs ++ testJobs ++ updateReadmeJobs ++ reportSuccessful ++ releaseJobs ++ postReleaseJobs) + .map(job => job.id -> job): _* + ) ) + val yaml: String = workflow.toJsonAST.flatMap(_.toYaml(yamlOptions).left.map(_.getMessage())) match { + case Right(value) => value + case Left(error) => sys.error(s"Error generating workflow yaml: $error") + } + val template = s"""|# This file was autogenerated using `zio-sbt-ci` plugin via `sbt ciGenerateGithubWorkflow` |# task and should be included in the git repository. Please do not edit it manually. | - |$workflow""".stripMargin + |$yaml""".stripMargin IO.write(new File(s".github/workflows/${ciWorkflowName.value.toLowerCase}.yml"), template) } + val IsSingleBuild: Def.Initialize[Boolean] = BuildAssertions.scalaCrossVersionsCountsOnePerProject + val CrossbuildOrNot: Def.Initialize[String] = Def.setting { + if (IsSingleBuild.value) "" else "+" + } + override lazy val buildSettings: Seq[Setting[_]] = Seq( - ciWorkflowName := "CI", - ciEnabledBranches := Seq.empty, - ciGenerateGithubWorkflow := generateGithubWorkflowTask.value, - ciDocsVersioningScheme := DocsVersioning.SemanticVersioning, - ciCheckGithubWorkflow := checkGithubWorkflowTask.value, - ciTargetScalaVersions := Map.empty, - ciTargetMinJavaVersions := Map.empty, - ciJvmOptions := Seq.empty, - ciNodeOptions := Seq.empty, - ciUpdateReadmeCondition := None, - ciGroupSimilarTests := false, - ciSwapSizeGB := 0, - ciTargetJavaVersions := Seq("11", "17", "21"), + ciWorkflowName := "CI", + ciEnabledBranches := Seq.empty, + ciGenerateGithubWorkflow := generateGithubWorkflowTask.value, + ciDocsVersioningScheme := DocsVersioning.SemanticVersioning, + ciCheckGithubWorkflow := checkGithubWorkflowTask.value, + ciTargetScalaVersions := Map.empty, + ciTargetMinJavaVersions := Map.empty, + ciJvmOptions := Seq.empty, + ciNodeOptions := Seq.empty, + ciUpdateReadmeCondition := None, + ciGroupSimilarTests := false, + ciSwapSizeGB := 0, + ciTargetJavaVersions := CiVersions.TargetJavaVersions, + ciDefaultJavaDistribution := CiVersions.JavaDistribution, + ciDefaultNodeJSVersion := CiVersions.NodeJS, ciCheckArtifactsBuildSteps := Seq( Step.SingleStep( name = "Check artifacts build process", - run = Some("sbt +publishLocal") + run = Some(s"sbt ${CrossbuildOrNot.value}publishLocal") ) ), ciCheckWebsiteBuildProcess := CheckWebsiteBuildProcess.value, - ciCheckArtifactsCompilationSteps := Seq( - Step.SingleStep( - name = "Check all code compiles", - run = Some(makePrefixJobs(ciBackgroundJobs.value) + "sbt +Test/compile") - ) + ciCheckArtifactsCompilationSteps := ( + Def.settingDyn { + val compileCommand = s"sbt ${CrossbuildOrNot.value}Test/compile" + Def.setting( + Seq( + Step.SingleStep( + name = "Check all code compiles", + run = Some(makePrefixJobs(ciBackgroundJobs.value) + compileCommand) + ) + ) + ) + }.value ), ciCheckGithubWorkflowSteps := Seq( Step.SingleStep( @@ -597,7 +683,7 @@ object ZioSbtCiPlugin extends AutoPlugin { ), ciBackgroundJobs := Seq.empty, ciMatrixMaxParallel := None, - ciDefaultJavaVersion := "17", + ciDefaultJavaVersion := zio.sbt.JavaVersion.`17`, ciBuildJobs := buildJobs.value, ciLintJobs := lintJobs.value, ciTestJobs := testJobs.value, @@ -641,7 +727,7 @@ object ZioSbtCiPlugin extends AutoPlugin { Step.SingleStep( name = "Set Swap Space", uses = Some(ActionRef(V("pierotofy/set-swap-space"))), - parameters = Map("swap-size-gb" -> swapSizeGB.asJson) + `with` = Some(Map("swap-size-gb" -> swapSizeGB.toString.toJsonAST.right.get)) ) } @@ -650,7 +736,7 @@ object ZioSbtCiPlugin extends AutoPlugin { Step.SingleStep( name = "Git Checkout", uses = Some(ActionRef(V("actions/checkout"))), - parameters = Map("fetch-depth" -> "0".asJson) + `with` = Some(Map("fetch-depth" -> "0".toJsonAST.right.get)) ) } @@ -659,13 +745,15 @@ object ZioSbtCiPlugin extends AutoPlugin { run = Some("sudo apt-get update && sudo apt-get install -y libuv1-dev") ) - def SetupJava(version: String = "17"): Step.SingleStep = Step.SingleStep( + def SetupJava(version: String = "17", distribution: String = "corretto"): Step.SingleStep = Step.SingleStep( name = "Setup Scala", uses = Some(ActionRef(V("actions/setup-java"))), - parameters = Map( - "distribution" -> "corretto".asJson, - "java-version" -> version.asJson, - "check-latest" -> true.asJson + `with` = Some( + Map( + "distribution" -> distribution.toJsonAST.right.get, + "java-version" -> version.toJsonAST.right.get, + "check-latest" -> true.toJsonAST.right.get + ) ) ) @@ -702,24 +790,32 @@ object ZioSbtCiPlugin extends AutoPlugin { val prefixJobs = makePrefixJobs(backgroundJobs) + val isSingleBuild = IsSingleBuild.value + Step.SingleStep( name = "Release", run = Some(prefixJobs + "sbt ci-release"), - env = Map( - "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", - "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", - "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", - "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" + env = Some( + ListMap( + "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", + "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", + "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}", + "CI_RELEASE" -> (if (isSingleBuild) "publishSigned" else "+publishSigned"), + "CI_SNAPSHOT_RELEASE" -> (if (isSingleBuild) "publish" else "+publish") + ) ) ) } - val SetupNodeJs: Step.SingleStep = Step.SingleStep( + def SetupNodeJs(version: String = CiVersions.NodeJS): Step.SingleStep = Step.SingleStep( name = "Setup NodeJs", uses = Some(ActionRef(V("actions/setup-node"))), - parameters = Map( - "node-version" -> "16.x".asJson, - "registry-url" -> "https://registry.npmjs.org".asJson + `with` = Some( + Map( + "node-version" -> version.toJsonAST.right.get, + "registry-url" -> "https://registry.npmjs.org".toJsonAST.right.get + ) ) ) @@ -732,7 +828,7 @@ object ZioSbtCiPlugin extends AutoPlugin { Step.SingleStep( name = "Publish Docs to NPM Registry", run = Some(prefixJobs + s"sbt docs/${docsVersioning.npmCommand}"), - env = Map("NODE_AUTH_TOKEN" -> "${{ secrets.NPM_TOKEN }}") + env = Some(ListMap("NODE_AUTH_TOKEN" -> "${{ secrets.NPM_TOKEN }}")) ) } diff --git a/zio-sbt-ecosystem/build.sbt b/zio-sbt-ecosystem/build.sbt index c673cdb8..8a7b0624 100644 --- a/zio-sbt-ecosystem/build.sbt +++ b/zio-sbt-ecosystem/build.sbt @@ -1,34 +1,21 @@ // Build Server Plugins -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.3") // Linting Plugins addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") // Versioning and Release Plugins -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0") // Docs Plugins -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") -// Cross-Compiler Plugins -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") -addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.2") - -// Benchmarking Plugins -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") - // Binary Compatibility Plugin -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") -libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" -libraryDependencies += "dev.zio" %% "zio" % "2.1.8" -libraryDependencies += "io.circe" %% "circe-yaml" % "0.16.0" +libraryDependencies += "dev.zio" %% "zio" % "2.1.11" diff --git a/zio-sbt-ecosystem/src/main/scala/zio/sbt/Commands.scala b/zio-sbt-ecosystem/src/main/scala/zio/sbt/Commands.scala index f34c3e2a..29c7bc43 100644 --- a/zio-sbt-ecosystem/src/main/scala/zio/sbt/Commands.scala +++ b/zio-sbt-ecosystem/src/main/scala/zio/sbt/Commands.scala @@ -88,6 +88,9 @@ object Commands { val fmt: ComposableCommand = (quietOn >> "scalafmtSbt" >> "+scalafmt" >> "+Test / scalafmt") ?? ("fmt", "Formats source files using scalafmt.") + val checkMima: ComposableCommand = + (quietOn >> "+checkMima") ?? ("checkMima", "Checks binary compatibility against previous versions.") + // TODO: have the correct license headers val lint: ComposableCommand = { quietOn >> @@ -96,6 +99,7 @@ object Commands { "+scalafmtCheckAll" >> // "+headerCheckAll" >> fixLint >> + checkMima >> "disableStrictCompile" } ?? ( "lint", @@ -125,6 +129,7 @@ object Commands { lint.toItem, fmt.toItem, fix.toItem, + checkMima.toItem, prepare.toItem, // buildAll.toItem, prepare.toItem, diff --git a/zio-sbt-ecosystem/src/main/scala/zio/sbt/MimaSettings.scala b/zio-sbt-ecosystem/src/main/scala/zio/sbt/MimaSettings.scala new file mode 100644 index 00000000..6d162270 --- /dev/null +++ b/zio-sbt-ecosystem/src/main/scala/zio/sbt/MimaSettings.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt +import com.typesafe.tools.mima.plugin.MimaKeys._ +import sbt.Keys._ +import sbt._ +import sbtdynver.DynVerPlugin.autoImport.previousStableVersion + +import zio.sbt.BuildAssertions.Keys.isScalaJVM + +trait MimaSettings { + + lazy val checkMima: TaskKey[Unit] = taskKey[Unit]("Checks binary compatibility against previous versions.") +} + +object MimaSettings extends MimaSettings { + + def projectSettings: Seq[Setting[_]] = + Def.settings( + checkMima := { + Def.taskIf { + if (isScalaJVM.value && !((checkMima / skip).value || (publish / skip).value)) mimaReportBinaryIssues.value + else () + }.value + }, + mimaFailOnProblem := true, + mimaPreviousArtifacts := (ThisBuild / previousStableVersion).value + .map(organization.value %% moduleName.value % _) + .fold(Set.empty[ModuleID])(Set(_)) + ) + +} diff --git a/zio-sbt-ecosystem/src/main/scala/zio/sbt/ScalaCompilerSettings.scala b/zio-sbt-ecosystem/src/main/scala/zio/sbt/ScalaCompilerSettings.scala index 1fc5b90a..fc5b5184 100644 --- a/zio-sbt-ecosystem/src/main/scala/zio/sbt/ScalaCompilerSettings.scala +++ b/zio-sbt-ecosystem/src/main/scala/zio/sbt/ScalaCompilerSettings.scala @@ -19,12 +19,14 @@ package zio.sbt import explicitdeps.ExplicitDepsPlugin.autoImport._ import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ import sbt.Keys._ -import sbt.{Def, _} +import sbt.{JavaVersion => _, _} import sbtbuildinfo.BuildInfoPlugin.autoImport.{BuildInfoKey, buildInfoKeys, buildInfoPackage} -import sbtcrossproject.CrossPlugin.autoImport.{JVMPlatform, crossProjectPlatform} +import sbtcrossproject.CrossPlugin.autoImport.crossProjectPlatform import scalafix.sbt.ScalafixPlugin.autoImport.{scalafixDependencies, scalafixSemanticdb} +import zio.sbt.ScalaVersions.Keys.{scala212, scala213, scala3} import zio.sbt.Versions._ +import zio.sbt.ZioSbtCrossbuildPlugin.autoImport.javaPlatform trait ScalaCompilerSettings { @@ -79,19 +81,19 @@ trait ScalaCompilerSettings { } ) - def extraOptions(scalaVersion: String, javaPlatform: String, optimize: Boolean): Seq[String] = - CrossVersion.partialVersion(scalaVersion) match { + def extraOptions(scalaVersion: String, optimize: Boolean): Seq[String] = + (CrossVersion.partialVersion(scalaVersion) match { case Some((3, _)) => Seq( "-language:implicitConversions", "-Xignore-scala2-macros", - "-noindent", - s"-release:$javaPlatform" + "-Xmax-inlines:64", + "-Wunused:imports" ) case Some((2, 13)) => Seq( "-Ywarn-unused:params,-implicits", - s"-release:$javaPlatform" + "-Wunused:imports" ) ++ std2xOptions ++ optimizerOptions(optimize) case Some((2, 12)) => Seq( @@ -112,7 +114,7 @@ trait ScalaCompilerSettings { "242" ) ++ std2xOptions ++ optimizerOptions(optimize) case _ => Seq.empty - } + }) def platformSpecificSources(platform: String, conf: String, baseDirectory: File)(versions: String*): List[File] = for { @@ -136,29 +138,42 @@ trait ScalaCompilerSettings { platformSpecificSources(platform, conf, baseDir)(versions: _*) } - lazy val crossProjectSettings: Seq[Setting[Seq[File]]] = Seq( - Compile / unmanagedSourceDirectories ++= { - crossPlatformSources( - scalaVersion.value, - crossProjectPlatform.value.identifier, - "main", - baseDirectory.value - ) - }, - Test / unmanagedSourceDirectories ++= { - crossPlatformSources( - scalaVersion.value, - crossProjectPlatform.value.identifier, - "test", - baseDirectory.value - ) - } + private lazy val nonProjectMatrixSourceDirectories = Seq( + Compile / unmanagedSourceDirectories ++= Def.settingDyn { + if (crossProjectPlatform.?.value.isDefined) { + Def.setting { + crossPlatformSources( + scalaVersion.value, + crossProjectPlatform.value.identifier, + "main", + baseDirectory.value + ) + } + } else { + Def.setting(List.empty[File]) + } + }.value, + Test / unmanagedSourceDirectories ++= Def.settingDyn { + if (crossProjectPlatform.?.value.isDefined) { + Def.setting { + crossPlatformSources( + scalaVersion.value, + crossProjectPlatform.value.identifier, + "test", + baseDirectory.value + ) + } + } else { + Def.setting(List.empty[File]) + } + }.value ) + def crossProjectSettings: Seq[Setting[_]] = nonProjectMatrixSourceDirectories + def stdSettings( name: Option[String] = None, packageName: Option[String] = None, - javaPlatform: String = "11", enableKindProjector: Boolean = true, enableCrossProject: Boolean = false, enableScalafix: Boolean = true, @@ -169,17 +184,23 @@ trait ScalaCompilerSettings { case None => Seq.empty }) ++ Seq( - ZioSbtEcosystemPlugin.autoImport.javaPlatform := javaPlatform, + crossScalaVersions := { + if (BuildAssertions.Keys.isProjectMatrix.?.value.contains(true)) + Seq.empty + else + ScalaVersions.Keys.defaultCrossScalaVersions.?.value + .getOrElse(Seq(scala212.value, scala213.value, scala3.value)) + }, scalacOptions := ( scalacOptions.value ++ stdOptions ++ - extraOptions(Keys.scalaVersion.value, javaPlatform, optimize = !isSnapshot.value) ++ + extraOptions(Keys.scalaVersion.value, optimize = !isSnapshot.value) ++ ( if (turnCompilerWarningIntoErrors && sys.env.contains("CI")) Seq("-Xfatal-warnings") else Nil // to enable Scalafix locally - ) + ) ++ + Seq(s"-release:${javaPlatform.value}") ).distinct, - javacOptions := Seq("-source", javaPlatform, "-target", javaPlatform), // Compile / console / scalacOptions ~= { // _.filterNot(Set("-Xfatal-warnings")) // }, @@ -190,10 +211,9 @@ trait ScalaCompilerSettings { ) } else Seq.empty }, - Test / parallelExecution := scalaBinaryVersion.value != "3", + Test / parallelExecution := scalaBinaryVersion.value != "3", // why not parallel execution for Scala 3? incOptions ~= (_.withLogRecompileOnMacro(false)), - autoAPIMappings := true, - unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library") + autoAPIMappings := true ) ++ (if (enableCrossProject) crossProjectSettings else Seq.empty) ++ { packageName match { case Some(name) => buildInfoSettings(name) @@ -205,8 +225,16 @@ trait ScalaCompilerSettings { lazy val scalafixSettings: Seq[Def.Setting[_]] = Seq( - semanticdbEnabled := Keys.scalaBinaryVersion.value != "3", - semanticdbOptions += "-P:semanticdb:synthetics:on", + semanticdbEnabled := { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, 3)) => true + case Some((2, _)) => true + case _ => false + } + }, + semanticdbOptions ++= { + if (BuildAssertions.Keys.isScala3.value) Seq.empty else Seq("-P:semanticdb:synthetics:on") + }, semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version ThisBuild / scalafixDependencies ++= List( "com.github.vovapolu" %% "scaluzzi" % ScaluzziVersion @@ -217,7 +245,7 @@ trait ScalaCompilerSettings { def scalaReflectTestSettings: List[Setting[_]] = List( libraryDependencies ++= { if (scalaBinaryVersion.value == "3") - Seq("org.scala-lang" % "scala-reflect" % ZioSbtEcosystemPlugin.autoImport.scala213.value % Test) + Seq("org.scala-lang" % "scala-reflect" % ZioSbtCrossbuildPlugin.autoImport.scala213.value % Test) else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Test) } @@ -233,8 +261,7 @@ trait ScalaCompilerSettings { libraryDependencies ++= Seq( "dev.zio" %%% "zio-test" % ZioSbtEcosystemPlugin.autoImport.zioVersion.value % Test, "dev.zio" %%% "zio-test-sbt" % ZioSbtEcosystemPlugin.autoImport.zioVersion.value % Test - ), - testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") + ) ) else Seq.empty) ++ { if (enableStreaming) @@ -244,7 +271,7 @@ trait ScalaCompilerSettings { def buildInfoSettings(packageName: String): Seq[Setting[_]] = Seq( - buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion, isSnapshot), + buildInfoKeys := Seq[BuildInfoKey](organization, moduleName, name, version, scalaVersion, sbtVersion, isSnapshot), buildInfoPackage := packageName ) @@ -269,19 +296,32 @@ trait ScalaCompilerSettings { ) def jsSettings: Seq[Setting[_]] = Seq( - Test / fork := crossProjectPlatform.value == JVMPlatform // set fork to `true` on JVM to improve log readability, JS and Native need `false` + Test / fork := BuildAssertions + .requireJS( + false + ) + .value, + unusedCompileDependenciesFilter -= moduleFilter("org.scala-js") ) -// def jsSettings_ = Seq( -// libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.2.2", -// libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.2.2" -// ) + def jvmSettings: Seq[Setting[_]] = Seq( + Test / fork := BuildAssertions + .requireJVM( + true + ) + .value // set fork to `true` on JVM to improve log readability, JS and Native need `false` + ) def nativeSettings: Seq[Setting[_]] = Seq( - doc / skip := true, - Compile / doc / sources := Seq.empty, - Test / test := { val _ = (Test / compile).value; () }, - Test / fork := crossProjectPlatform.value == JVMPlatform // set fork to `true` on JVM to improve log readability, JS and Native need `false` + Test / fork := BuildAssertions + .requireNative( + false + ) + .value, + doc / skip := BuildAssertions.requireNative(true).value, + Compile / doc / sources := BuildAssertions.requireNative(Seq.empty).value, + unusedCompileDependenciesFilter -= moduleFilter("org.scala-native") + // Test / test := { val _ = (Test / compile).value; () } // ?? ) lazy val scalajs: Seq[Setting[_]] = diff --git a/zio-sbt-ecosystem/src/main/scala/zio/sbt/Versions.scala b/zio-sbt-ecosystem/src/main/scala/zio/sbt/Versions.scala index 0a971aa1..dd520513 100644 --- a/zio-sbt-ecosystem/src/main/scala/zio/sbt/Versions.scala +++ b/zio-sbt-ecosystem/src/main/scala/zio/sbt/Versions.scala @@ -16,17 +16,17 @@ package zio.sbt -import sbt._ +import sbt.{ScalaVersion => _, _} object Versions { val KindProjectorVersion = "0.13.3" val ScaluzziVersion = "0.1.23" - val scala3 = "3.3.3" - val scala212 = "2.12.19" - val scala213 = "2.13.13" + val Scala3 = ScalaVersion.Scala3 + val Scala212 = ScalaVersion.Scala212 + val Scala213 = ScalaVersion.Scala213 - val zioVersion = "2.0.21" + val zioVersion = "2.1.11" lazy val betterMonadFor: ModuleID = "com.olegpy" %% "better-monadic-for" % "0.3.1" } diff --git a/zio-sbt-ecosystem/src/main/scala/zio/sbt/ZioSbtEcosystemPlugin.scala b/zio-sbt-ecosystem/src/main/scala/zio/sbt/ZioSbtEcosystemPlugin.scala index 0d9e444c..6bbe6a0e 100644 --- a/zio-sbt-ecosystem/src/main/scala/zio/sbt/ZioSbtEcosystemPlugin.scala +++ b/zio-sbt-ecosystem/src/main/scala/zio/sbt/ZioSbtEcosystemPlugin.scala @@ -19,22 +19,25 @@ package zio.sbt import scala.collection.immutable.ListMap import com.jsuereth.sbtpgp.SbtPgp.autoImport._ +import com.typesafe.tools.mima.plugin.MimaPlugin import de.heikoseeberger.sbtheader.HeaderPlugin import org.scalafmt.sbt.ScalafmtPlugin import sbt.Keys._ import sbt.nio.Keys.{ReloadOnSourceChanges, onChangedBuildSource} import sbt.{Def, _} -import sbtbuildinfo.BuildInfoPlugin +import sbtdynver.DynVerPlugin import scalafix.sbt.ScalafixPlugin +import zio.sbt.ZioSbtShared.autoImport.{banners, usefulTasksAndSettings, welcomeMessage} + object ZioSbtEcosystemPlugin extends AutoPlugin { override def trigger = allRequirements override def requires: Plugins = - super.requires && HeaderPlugin && ScalafixPlugin && ScalafmtPlugin && BuildInfoPlugin + super.requires && HeaderPlugin && ScalafixPlugin && ScalafmtPlugin && MimaPlugin && DynVerPlugin && ZioSbtCrossbuildPlugin - object autoImport extends ScalaCompilerSettings { + object autoImport extends ScalaCompilerSettings with MimaSettings { def addCommand(commandString: List[String], name: String, description: String): Seq[Setting[_]] = { val cCommand = Commands.ComposableCommand(commandString, name, description) @@ -47,69 +50,35 @@ object ZioSbtEcosystemPlugin extends AutoPlugin { usefulTasksAndSettings += command.toItem ) - lazy val scala3: SettingKey[String] = settingKey[String]("Scala 3 version") - lazy val scala212: SettingKey[String] = settingKey[String]("Scala 2.12 version") - lazy val scala213: SettingKey[String] = settingKey[String]("Scala 2.13 version") lazy val zioVersion: SettingKey[String] = settingKey[String]("ZIO version") - lazy val javaPlatform: SettingKey[String] = settingKey[String]("java target platform, default is 11") - - val welcomeBannerEnabled: SettingKey[Boolean] = - settingKey[Boolean]("Indicates whether or not to enable the welcome banner.") - - val usefulTasksAndSettings: SettingKey[Map[String, String]] = settingKey[Map[String, String]]( - "A map of useful tasks and settings that will be displayed as part of the welcome banner." - ) - } import autoImport.* - private val defaultTasksAndSettings: Map[String, String] = Commands.ComposableCommand.makeHelp ++ ListMap( + private val defaultTasksAndSettings: ListMap[String, String] = Commands.ComposableCommand.makeHelp ++ ListMap( // "build" -> "Lints source files then strictly compiles and runs tests.", - "enableStrictCompile" -> "Enables strict compilation e.g. warnings become errors.", - "disableStrictCompile" -> "Disables strict compilation e.g. warnings are no longer treated as errors.", - "~compile" -> "Compiles all modules (file-watch enabled)", - "test" -> "Runs all tests", - """testOnly *.YourSpec -- -t \"YourLabel\"""" -> "Only runs tests with matching term e.g." + "enableStrictCompile" -> "Enables strict compilation e.g. warnings become errors.", + "disableStrictCompile" -> "Disables strict compilation e.g. warnings are no longer treated as errors.", + "~compile" -> "Compiles all modules (file-watch enabled)" ) - def welcomeMessage: Setting[String] = - onLoadMessage := { - if (welcomeBannerEnabled.value) { - import scala.Console - - val maxLen = usefulTasksAndSettings.value.keys.map(_.length).max - - def normalizedPadding(s: String) = " " * (maxLen - s.length) - - def item(text: String): String = s"${Console.GREEN}> ${Console.CYAN}$text${Console.RESET}" - - s"""|${Banner.trueColor(s"${name.value} v.${version.value}")} - |Useful sbt tasks: - |${usefulTasksAndSettings.value.map { case (task, description) => - s"${item(task)} ${normalizedPadding(task)}${description}" - } - .mkString("\n")} - """.stripMargin - - } else "" - } - override def projectSettings: Seq[Setting[_]] = - Commands.settings ++ welcomeMessage ++ Seq( - usefulTasksAndSettings := defaultTasksAndSettings, - welcomeBannerEnabled := true - ) ++ Tasks.settings // ++ Tasks.settings + Commands.settings ++ Tasks.settings ++ Seq( + banners ++= { + if (SharedTasks.isRoot.value) Seq(Banner.trueColor(s"${(ThisBuild / name).value} v.${version.value}")) + else Seq.empty + }, + usefulTasksAndSettings ++= { + if (SharedTasks.isRoot.value) ZioSbtEcosystemPlugin.defaultTasksAndSettings.toSeq else Seq.empty + }, + onLoadMessage := { + if (SharedTasks.isRoot.value) welcomeMessage.init.value else "" + } + ) ++ MimaSettings.projectSettings override def buildSettings: Seq[Def.Setting[_]] = super.buildSettings ++ Seq( - scala3 := Versions.scala3, - scala212 := Versions.scala212, - scala213 := Versions.scala213, - scalaVersion := scala213.value, - crossScalaVersions := Seq(scala212.value, scala213.value, scala3.value), - zioVersion := Versions.zioVersion, - javaPlatform := "11" + zioVersion := zioVersion.?.value.getOrElse(Versions.zioVersion) ) override def globalSettings: Seq[Def.Setting[_]] = @@ -124,9 +93,10 @@ object ZioSbtEcosystemPlugin extends AutoPlugin { s"scm:git:git@github.com:zio/${normalizedName}.git" ) ), - pgpPassphrase := sys.env.get("PGP_PASSPHRASE").map(_.toArray), - pgpPublicRing := file("/tmp/public.asc"), - pgpSecretRing := file("/tmp/secret.asc"), - onChangedBuildSource := ReloadOnSourceChanges + pgpPassphrase := sys.env.get("PGP_PASSPHRASE").map(_.toArray), + pgpPublicRing := file("/tmp/public.asc"), + pgpSecretRing := file("/tmp/secret.asc"), + onChangedBuildSource := ReloadOnSourceChanges, + usefulTasksAndSettings / aggregate := false ) } diff --git a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettings/build.sbt b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettings/build.sbt index 9ca9f65f..f523b7f0 100644 --- a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettings/build.sbt +++ b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettings/build.sbt @@ -1,3 +1,5 @@ +ThisBuild / name := "zio-sbt-ecosystem-test" + lazy val root = (project in file(".")) .settings( version := "0.1", diff --git a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/.github/workflows/ci.yml b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/.github/workflows/ci.yml index f861c9d0..b02f9e67 100644 --- a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/.github/workflows/ci.yml +++ b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: matrix: java: ['11', '17', '21'] # These version must be different than the versions in V.scala to verify that we are reading from the ci.yml file. - scala: ['2.12.19', '2.13.13', '3.3.3' ] + scala: ['2.12.20', '2.13.15', '3.3.4' ] steps: - uses: actions/checkout@v4.1.1 - uses: actions/setup-java@v3.13.0 diff --git a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/build.sbt b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/build.sbt index 9ca9f65f..f523b7f0 100644 --- a/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/build.sbt +++ b/zio-sbt-ecosystem/src/sbt-test/zio-sbt-ecosystem/verifySettingsWithCI/build.sbt @@ -1,3 +1,5 @@ +ThisBuild / name := "zio-sbt-ecosystem-test" + lazy val root = (project in file(".")) .settings( version := "0.1", diff --git a/zio-sbt-githubactions/build.sbt b/zio-sbt-githubactions/build.sbt index b7d36a60..b8e1d71f 100644 --- a/zio-sbt-githubactions/build.sbt +++ b/zio-sbt-githubactions/build.sbt @@ -1,2 +1,3 @@ -libraryDependencies += "dev.zio" %% "zio" % "2.1.8" -libraryDependencies += "io.circe" %% "circe-yaml" % "0.16.0" +libraryDependencies += "dev.zio" %% "zio" % "2.1.11" +libraryDependencies += "dev.zio" %% "zio-json" % "0.7.3" +libraryDependencies += "dev.zio" %% "zio-json-yaml" % "0.7.3" diff --git a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/ScalaWorkflow.scala b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/ScalaWorkflow.scala index a9270149..a14b39f1 100644 --- a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/ScalaWorkflow.scala +++ b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/ScalaWorkflow.scala @@ -16,12 +16,13 @@ package zio.sbt.githubactions -import io.circe.syntax._ +import scala.collection.immutable.ListMap -import zio.sbt.githubactions.ScalaWorkflow.JavaVersion.JDK11 +import zio.json._ // The original code of the githubactions package was originally copied from the zio-aws-codegen project: // https://github.com/zio/zio-aws/tree/master/zio-aws-codegen/src/main/scala/zio/aws/codegen/githubactions +@deprecated("Use zio.sbt.githubactionsnative.ScalaWorkflow instead", "0.4.x") object ScalaWorkflow { import Step._ @@ -30,7 +31,7 @@ object ScalaWorkflow { name = "Checkout current branch", uses = Some(ActionRef("actions/checkout@v2")), parameters = Map( - "fetch-depth" := fetchDepth + "fetch-depth" -> fetchDepth.toJsonAST.right.get ) ) @@ -39,10 +40,10 @@ object ScalaWorkflow { name = "Setup Java and Scala", uses = Some(ActionRef("olafurpg/setup-scala@v11")), parameters = Map( - "java-version" := (javaVersion match { + "java-version" -> (javaVersion match { case None => "${{ matrix.java }}" case Some(version) => version.asString - }) + }).toJsonAST.right.get ) ) @@ -51,11 +52,11 @@ object ScalaWorkflow { name = "Setup NodeJS", uses = Some(ActionRef("actions/setup-node@v3")), parameters = Map( - "node-version" := (javaVersion match { + "node-version" -> (javaVersion match { case None => "16.x" case Some(version) => version.asString - }), - "registry-url" := "https://registry.npmjs.org" + }).toJsonAST.right.get, + "registry-url" -> "https://registry.npmjs.org".toJsonAST.right.get ) ) @@ -76,13 +77,13 @@ object ScalaWorkflow { name = "Cache SBT", uses = Some(ActionRef("actions/cache@v2")), parameters = Map( - "path" := Seq( + "path" -> Seq( "~/.ivy2/cache", "~/.sbt", "~/.coursier/cache/v1", "~/.cache/coursier/v1" - ).mkString("\n"), - "key" := s"$osS-sbt-$scalaS-$${{ hashFiles('**/*.sbt') }}-$${{ hashFiles('**/build.properties') }}" + ).mkString("\n").toJsonAST.right.get, + "key" -> s"$osS-sbt-$scalaS-$${{ hashFiles('**/*.sbt') }}-$${{ hashFiles('**/build.properties') }}".toJsonAST.right.get ) ) } @@ -98,7 +99,7 @@ object ScalaWorkflow { parameters: List[String], heapGb: Int = 6, stackMb: Int = 16, - env: Map[String, String] = Map.empty + env: ListMap[String, String] = ListMap.empty ): Step = SingleStep( name, @@ -131,8 +132,8 @@ object ScalaWorkflow { s"Upload $id targets", uses = Some(ActionRef("actions/upload-artifact@v2")), parameters = Map( - "name" := s"target-$id-$osS-$scalaS-$javaS", - "path" := "targets.tar" + "name" -> s"target-$id-$osS-$scalaS-$javaS".toJsonAST.right.get, + "path" -> "targets.tar".toJsonAST.right.get ) ) ) @@ -155,7 +156,7 @@ object ScalaWorkflow { s"Download stored $id targets", uses = Some(ActionRef("actions/download-artifact@v2")), parameters = Map( - "name" := s"target-$id-$osS-$scalaS-$javaS" + "name" -> s"target-$id-$osS-$scalaS-$javaS".toJsonAST.right.get ) ), SingleStep( @@ -182,14 +183,14 @@ object ScalaWorkflow { SingleStep( "Load PGP secret", run = Some(".github/import-key.sh"), - env = Map("PGP_SECRET" -> "${{ secrets.PGP_SECRET }}") + env = ListMap("PGP_SECRET" -> "${{ secrets.PGP_SECRET }}") ) def turnstyle(): Step = SingleStep( "Turnstyle", uses = Some(ActionRef("softprops/turnstyle@v1")), - env = Map( + env = ListMap( "GITHUB_TOKEN" -> "${{ secrets.ADMIN_GITHUB_TOKEN }}" ) ) @@ -217,30 +218,40 @@ object ScalaWorkflow { case class ScalaVersion(version: String) - trait JavaVersion { - val asString: String + sealed trait JavaVersion { + def distribution: String + def version: String + def asString: String = s"$distribution:$version" } + object JavaVersion { - case class CorrettoJDK(javaVersion: String) extends JavaVersion { - override val asString: String = s"corretto:$javaVersion" + def apply(distribution: String, version: String): JavaVersion = CustomJDK(distribution, version) + + case class CustomJDK(distribution: String, version: String) extends JavaVersion + + case class CorrettoJDK(version: String) extends JavaVersion { + override def distribution: String = "corretto" + } + + object CorrettoJDK { + val `11`: JavaVersion = CorrettoJDK("11") + val `17`: JavaVersion = CorrettoJDK("17") + val `21`: JavaVersion = CorrettoJDK("21") } - val JDK11: JavaVersion = CorrettoJDK("11") - val JDK17: JavaVersion = CorrettoJDK("17") - val JDK21: JavaVersion = CorrettoJDK("21") } implicit class JobOps(job: Job) { def matrix( scalaVersions: Seq[ScalaVersion], operatingSystems: Seq[OS] = Seq(OS.UbuntuLatest), - javaVersions: Seq[JavaVersion] = Seq(JDK11) + javaVersions: Seq[JavaVersion] = Seq(JavaVersion.CorrettoJDK.`11`) ): Job = job.copy( strategy = Some( Strategy( - matrix = Map( + matrix = ListMap( "os" -> operatingSystems.map(_.asString).toList, "scala" -> scalaVersions.map(_.version).toList, "java" -> javaVersions.map(_.asString).toList diff --git a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/model.scala b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/model.scala index a3a6d142..7c9646a5 100644 --- a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/model.scala +++ b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactions/model.scala @@ -16,16 +16,29 @@ package zio.sbt.githubactions -import io.circe._ -import io.circe.syntax._ +import scala.collection.immutable.ListMap -import zio.sbt.githubactions.Step.StepSequence +import zio.json.ast.Json +import zio.sbt.{githubactionsnative => ghnative} -sealed trait OS { - val asString: String +abstract class OS(name: String) { + val asString: String = name } object OS { - case object UbuntuLatest extends OS { val asString = "ubuntu-latest" } + + def apply(name: String): OS = Custom(name) + + case class Custom(name: String) extends OS(name) + + case object UbuntuLatest extends OS("ubuntu-latest") + case object Ubuntu2404 extends OS("ubuntu-24.04") + case object Ubuntu2204 extends OS("ubuntu-22.04") + + implicit class OSOps(private val os: OS) extends AnyVal { + def toNative: ghnative.OS = os match { + case UbuntuLatest => ghnative.OS.UbuntuLatest + } + } } sealed trait Branch @@ -33,96 +46,97 @@ object Branch { case object All extends Branch case class Named(name: String) extends Branch - implicit val encoder: Encoder[Branch] = { - case All => Json.fromString("*") - case Named(name) => Json.fromString(name) + implicit class BranchOps(private val branch: Branch) extends AnyVal { + def toNative: ghnative.Branch = branch match { + case All => ghnative.Branch.All + case Named(name) => ghnative.Branch.Named(name) + } } } -sealed trait Trigger { - def toKeyValuePair: (String, Json) -} +sealed trait Trigger case class Input(key: String, description: String, required: Boolean, defaultValue: String) +object Input { + implicit class InputOps(private val input: Input) extends AnyVal { + def toNative: (String, ghnative.Trigger.InputValue) = + input.key -> ghnative.Trigger.InputValue(input.description, input.required, input.defaultValue) + } +} + object Trigger { case class WorkflowDispatch( inputs: Seq[Input] = Seq.empty - ) extends Trigger { - override def toKeyValuePair: (String, Json) = - "workflow_dispatch" := inputs.map { i => - i.key -> - Json.obj( - ("description", i.description.asJson), - ("required", i.required.asJson), - ("default", i.defaultValue.asJson) - ) - }.toMap.asJson - } + ) extends Trigger case class Release( releaseTypes: Seq[String] = Seq.empty - ) extends Trigger { - override def toKeyValuePair: (String, Json) = - "release" := Json.obj("types" := releaseTypes) - } + ) extends Trigger case class PullRequest( branches: Seq[Branch] = Seq.empty, ignoredBranches: Seq[Branch] = Seq.empty - ) extends Trigger { - override def toKeyValuePair: (String, Json) = - "pull_request" := Json.obj( - Seq( - "branches" := branches, - "branches-ignore" := ignoredBranches - ).filter { case (_, data) => data.asArray.exists(_.nonEmpty) }: _* - ) - } + ) extends Trigger case class Push( branches: Seq[Branch] = Seq.empty, ignoredBranches: Seq[Branch] = Seq.empty - ) extends Trigger { - override def toKeyValuePair: (String, Json) = - "push" := Json.obj( - Seq( - "branches" := branches, - "branches-ignore" := ignoredBranches - ).filter { case (_, data) => data.asArray.exists(_.nonEmpty) }: _* - ) - } + ) extends Trigger case class Create( branches: Seq[Branch] = Seq.empty, ignoredBranches: Seq[Branch] = Seq.empty - ) extends Trigger { - override def toKeyValuePair: (String, Json) = - "create" := Json.obj( - Seq( - "branches" := branches, - "branches-ignore" := ignoredBranches - ).filter { case (_, data) => data.asArray.exists(_.nonEmpty) }: _* - ) + ) extends Trigger + + implicit class TriggerOps(private val trigger: Trigger) extends AnyVal { + def toNative: ghnative.Trigger = trigger match { + case WorkflowDispatch(inputs) => + ghnative.Trigger.WorkflowDispatch(Some(ListMap(inputs.map(_.toNative): _*)).filter(_.nonEmpty)) + case Release(releaseTypes) => + ghnative.Trigger.Release(releaseTypes.map { + case "created" => ghnative.Trigger.ReleaseType.Created + case "published" => ghnative.Trigger.ReleaseType.Published + case "prereleased" => ghnative.Trigger.ReleaseType.Prereleased + }) + case PullRequest(branches, ignoredBranches) => + ghnative.Trigger.PullRequest( + Some(branches.map(_.toNative)).filter(_.nonEmpty), + Some(ignoredBranches.map(_.toNative)).filter(_.nonEmpty) + ) + case Push(branches, ignoredBranches) => + ghnative.Trigger.Push( + Some(branches.map(_.toNative)).filter(_.nonEmpty), + Some(ignoredBranches.map(_.toNative)).filter(_.nonEmpty) + ) + case Create(branches, ignoredBranches) => + ghnative.Trigger.Create( + Some(branches.map(_.toNative)).filter(_.nonEmpty), + Some(ignoredBranches.map(_.toNative)).filter(_.nonEmpty) + ) + } } } -case class Strategy(matrix: Map[String, List[String]], maxParallel: Option[Int] = None, failFast: Boolean = true) +case class Strategy(matrix: ListMap[String, List[String]], maxParallel: Option[Int] = None, failFast: Boolean = true) object Strategy { - implicit val encoder: Encoder[Strategy] = - (s: Strategy) => - Json.obj( - "fail-fast" := s.failFast, - "max-parallel" := s.maxParallel, - "matrix" := s.matrix + implicit class StrategyOps(private val strategy: Strategy) extends AnyVal { + def toNative: ghnative.Strategy = + ghnative.Strategy( + matrix = strategy.matrix.map { case (key, values) => key -> values }, + maxParallel = strategy.maxParallel, + failFast = strategy.failFast ) + } } case class ActionRef(ref: String) + object ActionRef { - implicit val encoder: Encoder[ActionRef] = - (action: ActionRef) => Json.fromString(action.ref) + implicit class ActionRefOps(private val actionRef: ActionRef) extends AnyVal { + def toNative: ghnative.ActionRef = ghnative.ActionRef(actionRef.ref) + } } sealed trait Condition { @@ -162,8 +176,12 @@ object Condition { def asString: String = expression } - implicit val encoder: Encoder[Condition] = - (c: Condition) => Json.fromString(c.asString) + implicit class ConditionOps(private val condition: Condition) extends AnyVal { + def toNative: ghnative.Condition = condition match { + case Expression(expression) => ghnative.Condition.Expression(expression) + case Function(expression) => ghnative.Condition.Function(expression) + } + } } sealed trait Step { @@ -178,7 +196,7 @@ object Step { condition: Option[Condition] = None, parameters: Map[String, Json] = Map.empty, run: Option[String] = None, - env: Map[String, String] = Map.empty + env: ListMap[String, String] = ListMap.empty ) extends Step { override def when(condition: Condition): Step = copy(condition = Some(condition)) @@ -194,31 +212,38 @@ object Step { steps.flatMap(_.flatten) } - implicit val encoder: Encoder[SingleStep] = - (s: SingleStep) => - Json - .obj( - "name" := s.name, - "id" := s.id, - "uses" := s.uses, - "if" := s.condition, - "with" := (if (s.parameters.nonEmpty) s.parameters.asJson - else Json.Null), - "run" := s.run, - "env" := (if (s.env.nonEmpty) s.env.asJson else Json.Null) + implicit class StepOps(private val step: Step) extends AnyVal { + def toNative: ghnative.Step = step match { + case SingleStep(name, id, uses, condition, parameters, run, env) => + ghnative.Step.SingleStep( + name = name, + id = id, + uses = uses.map(_.toNative), + `if` = condition.map(_.toNative), + `with` = Some(parameters).filter(_.nonEmpty), + run = run, + env = Some(env).filter(_.nonEmpty) ) + case StepSequence(steps) => + ghnative.Step.StepSequence(steps.map(_.toNative)) + } + } } case class ImageRef(ref: String) + object ImageRef { - implicit val encoder: Encoder[ImageRef] = - (image: ImageRef) => Json.fromString(image.ref) + implicit class ImageRefOps(private val imageRef: ImageRef) extends AnyVal { + def toNative: ghnative.ImageRef = ghnative.ImageRef(imageRef.ref) + } } case class ServicePort(inner: Int, outer: Int) + object ServicePort { - implicit val encoder: Encoder[ServicePort] = - (sp: ServicePort) => Json.fromString(s"${sp.inner}:${sp.outer}") + implicit class ServicePortOps(private val servicePort: ServicePort) extends AnyVal { + def toNative: ghnative.ServicePort = ghnative.ServicePort(servicePort.inner, servicePort.outer) + } } case class Service( @@ -227,14 +252,16 @@ case class Service( env: Map[String, String] = Map.empty, ports: Seq[ServicePort] = Seq.empty ) + object Service { - implicit val encoder: Encoder[Service] = - (s: Service) => - Json.obj( - "image" := s.image, - "env" := s.env, - "ports" := s.ports - ) + implicit class ServiceOps(private val service: Service) extends AnyVal { + def toNative: ghnative.Service = ghnative.Service( + name = service.name, + image = service.image.toNative, + env = Some(service.env).filter(_.nonEmpty), + ports = Some(service.ports.map(_.toNative)).filter(_.nonEmpty) + ) + } } case class Job( @@ -260,26 +287,21 @@ case class Job( } object Job { - implicit val encoder: Encoder[Job] = - (job: Job) => - Json - .obj( - "name" := job.name, - "runs-on" := job.runsOn, - "continue-on-error" := job.continueOnError, - "strategy" := job.strategy, - "needs" := (if (job.need.nonEmpty) job.need.asJson - else Json.Null), - "services" := (if (job.services.nonEmpty) { - Json.obj( - job.services.map(svc => svc.name := svc): _* - ) - } else { - Json.Null - }), - "if" := job.condition, - "steps" := StepSequence(job.steps).flatten - ) + implicit class JobOps(private val job: Job) extends AnyVal { + def toNative: ghnative.Job = + ghnative.Job( + name = job.name, + runsOn = job.runsOn, + timeoutMinutes = Some(job.timeoutMinutes), + continueOnError = job.continueOnError, + strategy = job.strategy.map(_.toNative), + steps = job.steps.map(_.toNative).flatMap(_.flatten), + needs = Some(job.need), + services = Some(job.services.map(_.toNative)).filter(_.nonEmpty), + `if` = job.condition.map(_.toNative) + ) + + } } case class Workflow( @@ -302,26 +324,24 @@ case class Workflow( } object Workflow { - implicit val encoder: Encoder[Workflow] = - (wf: Workflow) => - Json - .obj( - "name" := wf.name, - "env" := wf.env, - "on" := (if (wf.triggers.isEmpty) - Json.Null - else { - Json.obj( - wf.triggers - .map(_.toKeyValuePair): _* - ) - }), - "concurrency" := Json.obj( - "group" := Json.fromString( - "${{ github.workflow }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.run_id || github.ref }}" - ), - "cancel-in-progress" := true - ), - "jobs" := Json.obj(wf.jobs.map(job => job.id := job): _*) - ) + implicit class WorkflowOps(private val workflow: Workflow) extends AnyVal { + def toNative: ghnative.Workflow = ghnative.Workflow( + name = workflow.name, + env = Some(ListMap.empty ++ workflow.env).filter(_.nonEmpty), + on = { + val triggers = workflow.triggers.map(_.toNative) + Some( + ghnative.Triggers( + workflowDispatch = triggers.collectFirst { case t: ghnative.Trigger.WorkflowDispatch => t } + .getOrElse(ghnative.Trigger.WorkflowDispatch()), + release = triggers.collectFirst { case t: ghnative.Trigger.Release => t }, + pullRequest = triggers.collectFirst { case t: ghnative.Trigger.PullRequest => t }, + push = triggers.collectFirst { case t: ghnative.Trigger.Push => t }, + create = triggers.collectFirst { case t: ghnative.Trigger.Create => t } + ) + ).filter(_ => triggers.nonEmpty) + }, + jobs = ListMap(workflow.jobs.map(_.toNative).map(job => job.id -> job): _*) + ) + } } diff --git a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/ScalaWorkflow.scala b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/ScalaWorkflow.scala new file mode 100644 index 00000000..5b607976 --- /dev/null +++ b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/ScalaWorkflow.scala @@ -0,0 +1,276 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt.githubactionsnative + +import scala.collection.immutable.ListMap + +import zio.json._ + +// The original code of the githubactions package was originally copied from the zio-aws-codegen project: +// https://github.com/zio/zio-aws/tree/master/zio-aws-codegen/src/main/scala/zio/aws/codegen/githubactions +object ScalaWorkflow { + import Step._ + + def checkoutCurrentBranch(fetchDepth: Int = 0): Step = + SingleStep( + name = "Checkout current branch", + uses = Some(ActionRef("actions/checkout@v2")), + `with` = Some( + Map( + "fetch-depth" -> fetchDepth.toJsonAST.right.get + ) + ) + ) + + def setupScala(javaVersion: Option[JavaVersion] = None): Step = + SingleStep( + name = "Setup Java and Scala", + uses = Some(ActionRef("olafurpg/setup-scala@v11")), + `with` = Some( + Map( + "java-version" -> (javaVersion match { + case None => "${{ matrix.java }}" + case Some(version) => version.asString + }).toJsonAST.right.get + ) + ) + ) + + def setupNode(javaVersion: Option[JavaVersion] = None): Step = + SingleStep( + name = "Setup NodeJS", + uses = Some(ActionRef("actions/setup-node@v3")), + `with` = Some( + Map( + "node-version" -> (javaVersion match { + case None => "16.x" + case Some(version) => version.asString + }).toJsonAST.right.get, + "registry-url" -> "https://registry.npmjs.org".toJsonAST.right.get + ) + ) + ) + + def setupGPG(): Step = + SingleStep( + "Setup GPG", + uses = Some(ActionRef("olafurpg/setup-gpg@v3")) + ) + + def cacheSBT( + os: Option[OS] = None, + scalaVersion: Option[ScalaVersion] = None + ): Step = { + val osS = os.map(_.asString).getOrElse("${{ matrix.os }}") + val scalaS = scalaVersion.map(_.version).getOrElse("${{ matrix.scala }}") + + SingleStep( + name = "Cache SBT", + uses = Some(ActionRef("actions/cache@v2")), + `with` = Some( + Map( + "path" -> Seq( + "~/.ivy2/cache", + "~/.sbt", + "~/.coursier/cache/v1", + "~/.cache/coursier/v1" + ).mkString("\n").toJsonAST.right.get, + "key" -> s"$osS-sbt-$scalaS-$${{ hashFiles('**/*.sbt') }}-$${{ hashFiles('**/build.properties') }}".toJsonAST.right.get + ) + ) + ) + } + + def setupGitUser(): Step = + SingleStep( + name = "Setup GIT user", + uses = Some(ActionRef("fregante/setup-git-user@v1")) + ) + + def runSBT( + name: String, + parameters: List[String], + heapGb: Int = 6, + stackMb: Int = 16, + env: ListMap[String, String] = ListMap.empty + ): Step = + SingleStep( + name, + run = Some( + s"sbt -J-XX:+UseG1GC -J-Xmx${heapGb}g -J-Xms${heapGb}g -J-Xss${stackMb}m ${parameters.mkString(" ")}" + ), + env = Some(env).filter(_.nonEmpty) + ) + + def storeTargets( + id: String, + directories: List[String], + os: Option[OS] = None, + scalaVersion: Option[ScalaVersion] = None, + javaVersion: Option[JavaVersion] = None + ): Step = { + val osS = os.map(_.asString).getOrElse("${{ matrix.os }}") + val scalaS = scalaVersion.map(_.version).getOrElse("${{ matrix.scala }}") + val javaS = javaVersion.map(_.asString).getOrElse("${{ matrix.java }}") + + StepSequence( + Seq( + SingleStep( + s"Compress $id targets", + run = Some( + s"tar cvf targets.tar ${directories.map(dir => s"$dir/target".dropWhile(_ == '/')).mkString(" ")}" + ) + ), + SingleStep( + s"Upload $id targets", + uses = Some(ActionRef("actions/upload-artifact@v2")), + `with` = Some( + Map( + "name" -> s"target-$id-$osS-$scalaS-$javaS".toJsonAST.right.get, + "path" -> "targets.tar".toJsonAST.right.get + ) + ) + ) + ) + ) + } + + def loadStoredTarget( + id: String, + os: Option[OS] = None, + scalaVersion: Option[ScalaVersion] = None, + javaVersion: Option[JavaVersion] = None + ): Step = { + val osS = os.map(_.asString).getOrElse("${{ matrix.os }}") + val scalaS = scalaVersion.map(_.version).getOrElse("${{ matrix.scala }}") + val javaS = javaVersion.map(_.asString).getOrElse("${{ matrix.java }}") + + StepSequence( + Seq( + SingleStep( + s"Download stored $id targets", + uses = Some(ActionRef("actions/download-artifact@v2")), + `with` = Some( + Map( + "name" -> s"target-$id-$osS-$scalaS-$javaS".toJsonAST.right.get + ) + ) + ), + SingleStep( + s"Inflate $id targets", + run = Some( + "tar xvf targets.tar\nrm targets.tar" + ) + ) + ) + ) + } + + def loadStoredTargets( + ids: List[String], + os: Option[OS] = None, + scalaVersion: Option[ScalaVersion] = None, + javaVersion: Option[JavaVersion] = None + ): Step = + StepSequence( + ids.map(loadStoredTarget(_, os, scalaVersion, javaVersion)) + ) + + def loadPGPSecret(): Step = + SingleStep( + "Load PGP secret", + run = Some(".github/import-key.sh"), + env = Some(ListMap("PGP_SECRET" -> "${{ secrets.PGP_SECRET }}")) + ) + + def turnstyle(): Step = + SingleStep( + "Turnstyle", + uses = Some(ActionRef("softprops/turnstyle@v1")), + env = Some( + ListMap( + "GITHUB_TOKEN" -> "${{ secrets.ADMIN_GITHUB_TOKEN }}" + ) + ) + ) + + def collectDockerLogs(): Step = + SingleStep( + "Collect Docker logs", + uses = Some(ActionRef("jwalton/gh-docker-logs@v1")) + ) + + val isMaster: Condition = Condition.Expression( + "github.ref == 'refs/heads/master'" + ) + val isNotMaster: Condition = Condition.Expression( + "github.ref != 'refs/heads/master'" + ) + def isScalaVersion(version: ScalaVersion): Condition = Condition.Expression( + s"matrix.scala == '${version.version}'" + ) + def isNotScalaVersion(version: ScalaVersion): Condition = + Condition.Expression( + s"matrix.scala != '${version.version}'" + ) + val isFailure: Condition = Condition.Function("failure()") + + case class ScalaVersion(version: String) + + sealed trait JavaVersion { + def distribution: String + def version: String + def asString: String = s"$distribution:$version" + } + + object JavaVersion { + + def apply(distribution: String, version: String): JavaVersion = CustomJDK(distribution, version) + + case class CustomJDK(distribution: String, version: String) extends JavaVersion + + case class CorrettoJDK(version: String) extends JavaVersion { + override def distribution: String = "corretto" + } + + object CorrettoJDK { + val `11`: JavaVersion = CorrettoJDK("11") + val `17`: JavaVersion = CorrettoJDK("17") + val `21`: JavaVersion = CorrettoJDK("21") + } + } + + implicit class JobOps(job: Job) { + def matrix( + scalaVersions: Seq[ScalaVersion], + operatingSystems: Seq[OS] = Seq(OS.UbuntuLatest), + javaVersions: Seq[JavaVersion] = Seq(JavaVersion.CorrettoJDK.`11`) + ): Job = + job.copy( + strategy = Some( + Strategy( + matrix = ListMap( + "os" -> operatingSystems.map(_.asString).toList, + "scala" -> scalaVersions.map(_.version).toList, + "java" -> javaVersions.map(_.asString).toList + ) + ) + ), + runsOn = "${{ matrix.os }}" + ) + } +} diff --git a/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/model.scala b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/model.scala new file mode 100644 index 00000000..48ebba09 --- /dev/null +++ b/zio-sbt-githubactions/src/main/scala/zio/sbt/githubactionsnative/model.scala @@ -0,0 +1,376 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt.githubactionsnative + +import scala.collection.immutable.ListMap +import scala.util.{Failure, Success, Try} + +import zio.json._ +import zio.json.ast.Json + +abstract class OS(name: String) { + val asString: String = name +} +object OS { + + def apply(name: String): OS = Custom(name) + + case class Custom(name: String) extends OS(name) + + case object UbuntuLatest extends OS("ubuntu-latest") + case object Ubuntu2404 extends OS("ubuntu-24.04") + case object Ubuntu2204 extends OS("ubuntu-22.04") +} + +sealed trait Branch +object Branch { + case object All extends Branch + case class Named(name: String) extends Branch + + implicit lazy val codec: JsonCodec[Branch] = JsonCodec.string.transform( + { + case "*" => All + case name => Named(name) + }, + { + case All => "*" + case Named(name) => name + } + ) +} + +@jsonMemberNames(SnakeCase) +case class Triggers( + workflowDispatch: Trigger.WorkflowDispatch = Trigger.WorkflowDispatch(), + release: Option[Trigger.Release] = None, + pullRequest: Option[Trigger.PullRequest] = None, + push: Option[Trigger.Push] = None, + create: Option[Trigger.Create] = None +) + +object Triggers { + + implicit lazy val codec: JsonCodec[Triggers] = DeriveJsonCodec.gen[Triggers] +} + +sealed trait Trigger + +object Trigger { + case class InputValue(description: String, required: Boolean, default: String) + object InputValue { + implicit lazy val jsonCodec: JsonCodec[InputValue] = DeriveJsonCodec.gen[InputValue] + } + + case class WorkflowDispatch( + inputs: Option[ListMap[String, InputValue]] = None + ) extends Trigger + + object WorkflowDispatch { + implicit def listMapCodec[K: JsonFieldDecoder: JsonFieldEncoder, V: JsonCodec]: JsonCodec[ListMap[K, V]] = + JsonCodec( + JsonEncoder.keyValueIterable[K, V, ListMap], + JsonDecoder.keyValueChunk[K, V].map(c => ListMap(c: _*)) + ) + + implicit lazy val jsonCodec: JsonCodec[WorkflowDispatch] = DeriveJsonCodec.gen[WorkflowDispatch] + } + + case class Release( + types: Seq[ReleaseType] = Seq.empty + ) extends Trigger + + object Release { + implicit lazy val jsonCodec: JsonCodec[Release] = DeriveJsonCodec.gen[Release] + } + + sealed trait ReleaseType + object ReleaseType { + case object Created extends ReleaseType + case object Published extends ReleaseType + case object Prereleased extends ReleaseType + + implicit lazy val codec: JsonCodec[ReleaseType] = JsonCodec.string.transformOrFail( + { + case "created" => Right(Created) + case "published" => Right(Published) + case "prereleased" => Right(Prereleased) + case other => Left(s"Invalid release type: $other") + }, + { + case Created => "created" + case Published => "published" + case Prereleased => "prereleased" + } + ) + } + + @jsonMemberNames(KebabCase) + case class PullRequest( + // types: Option[Seq[PullRequestType]] = None, + branches: Option[Seq[Branch]] = None, + branchesIgnore: Option[Seq[Branch]] = None, + paths: Option[Seq[String]] = None + ) extends Trigger + + object PullRequest { + implicit lazy val jsonCodec: JsonCodec[PullRequest] = DeriveJsonCodec.gen[PullRequest] + } + + case class Push( + branches: Option[Seq[Branch]] = None, + branchesIgnore: Option[Seq[Branch]] = None + ) extends Trigger + + object Push { + implicit lazy val jsonCodec: JsonCodec[Push] = DeriveJsonCodec.gen[Push] + } + + case class Create( + branches: Option[Seq[Branch]] = None, + branchesIgnore: Option[Seq[Branch]] = None + ) extends Trigger + + object Create { + implicit lazy val jsonCodec: JsonCodec[Create] = DeriveJsonCodec.gen[Create] + } +} + +@jsonMemberNames(KebabCase) +case class Strategy(matrix: ListMap[String, List[String]], maxParallel: Option[Int] = None, failFast: Boolean = true) + +object Strategy { + import Workflow.listMapCodec + + implicit lazy val codec: JsonCodec[Strategy] = DeriveJsonCodec.gen[Strategy] +} + +case class ActionRef(ref: String) +object ActionRef { + implicit lazy val codec: JsonCodec[ActionRef] = JsonCodec.string.transform(ActionRef(_), _.ref) +} + +sealed trait Condition { + def &&(other: Condition): Condition + def ||(other: Condition): Condition + def asString: String +} + +object Condition { + case class Expression(expression: String) extends Condition { + def &&(other: Condition): Condition = + other match { + case Expression(otherExpression: String) => + Expression(s"($expression) && ($otherExpression)") + case Function(_: String) => + throw new IllegalArgumentException("Not supported currently") + } + + def ||(other: Condition): Condition = + other match { + case Expression(otherExpression: String) => + Expression(s"($expression) || ($otherExpression)") + case Function(_: String) => + throw new IllegalArgumentException("Not supported currently") + } + + def asString: String = s"$${{ $expression }}" + } + + object Expression { + implicit lazy val codec: JsonCodec[Expression] = JsonCodec.string.transform(Expression(_), _.asString) + } + + case class Function(expression: String) extends Condition { + def &&(other: Condition): Condition = + throw new IllegalArgumentException("Not supported currently") + + def ||(other: Condition): Condition = + throw new IllegalArgumentException("Not supported currently") + + def asString: String = expression + } + + object Function { + implicit lazy val codec: JsonCodec[Function] = JsonCodec.string.transform(Function(_), _.expression) + } + + implicit lazy val codec: JsonCodec[Condition] = JsonCodec.string.transform( + { + case expression if expression.startsWith("${{") => Expression(expression) + case expression => Function(expression) + }, + _.asString + ) +} + +sealed trait Step { + def when(condition: Condition): Step + def flatten: Seq[Step.SingleStep] +} +object Step { + case class SingleStep( + name: String, + id: Option[String] = None, + uses: Option[ActionRef] = None, + `if`: Option[Condition] = None, + `with`: Option[Map[String, Json]] = None, + run: Option[String] = None, + env: Option[ListMap[String, String]] = None + ) extends Step { + override def when(condition: Condition): Step = + copy(`if` = Some(condition)) + + override def flatten: Seq[Step.SingleStep] = Seq(this) + } + + object SingleStep { + import Workflow.listMapCodec + + implicit lazy val codec: JsonCodec[SingleStep] = DeriveJsonCodec.gen[SingleStep] + } + + case class StepSequence(steps: Seq[Step]) extends Step { + override def when(condition: Condition): Step = + copy(steps = steps.map(_.when(condition))) + + override def flatten: Seq[SingleStep] = + steps.flatMap(_.flatten) + } + + implicit lazy val codec: JsonCodec[Step] = DeriveJsonCodec.gen[Step] +} + +case class ImageRef(ref: String) +object ImageRef { + implicit lazy val codec: JsonCodec[ImageRef] = JsonCodec.string.transform(ImageRef(_), _.ref) +} + +case class ServicePort(inner: Int, outer: Int) +object ServicePort { + implicit lazy val codec: JsonCodec[ServicePort] = JsonCodec.string.transformOrFail( + v => + Try(v.split(":", 2).map(_.toInt).toList) match { + case Success(inner :: outer :: Nil) => Right(ServicePort(inner.toInt, outer.toInt)) + case Success(_) => Left("Invalid service port format: " + v) + case Failure(_) => Left("Invalid service port format: " + v) + }, + sp => s"${sp.inner}:${sp.outer}" + ) +} + +case class Service( + name: String, + image: ImageRef, + env: Option[Map[String, String]] = None, + ports: Option[Seq[ServicePort]] = None +) +object Service { + implicit lazy val codec: JsonCodec[Service] = DeriveJsonCodec.gen[Service] +} + +@jsonMemberNames(KebabCase) +case class Job( + name: String, + runsOn: String = "ubuntu-latest", + timeoutMinutes: Option[Int] = None, + continueOnError: Boolean = false, + strategy: Option[Strategy] = None, + needs: Option[Seq[String]] = None, + services: Option[Seq[Service]] = None, + `if`: Option[Condition] = None, + steps: Seq[Step.SingleStep] = Seq.empty +) { + + def id: String = name.toLowerCase().replace(" ", "-") + + def withStrategy(strategy: Strategy): Job = + copy(strategy = Some(strategy)) + + def withSteps(steps: Step*): Job = steps match { + case steps: Step.StepSequence => + copy(steps = steps.flatten) + case step: Step.SingleStep => + copy(steps = step :: Nil) + } + + def withServices(services: Service*): Job = + copy(services = Some(services)) + + def withRunsOn(runsOn: String): Job = + copy(runsOn = runsOn) + + def withName(name: String): Job = + copy(name = name) + + def withTimeoutMinutes(timeoutMinutes: Option[Int]): Job = + copy(timeoutMinutes = timeoutMinutes) + + def withContinueOnError(continueOnError: Boolean): Job = + copy(continueOnError = continueOnError) + + def withStrategy(strategy: Option[Strategy]): Job = + copy(strategy = strategy) + + def withNeeds(needs: Option[Seq[String]]): Job = + copy(needs = needs) +} + +object Job { + implicit lazy val codec: JsonCodec[Job] = DeriveJsonCodec.gen[Job] +} + +@jsonMemberNames(KebabCase) +case class Concurrency( + group: String, + cancelInProgress: Boolean = true +) + +object Concurrency { + implicit lazy val codec: JsonCodec[Concurrency] = DeriveJsonCodec.gen[Concurrency] +} + +case class Workflow( + name: String, + env: Option[ListMap[String, String]] = None, + on: Option[Triggers] = None, + concurrency: Concurrency = Concurrency( + "${{ github.workflow }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.run_id || github.ref }}" + ), + jobs: ListMap[String, Job] = ListMap.empty +) { + def withOn(on: Triggers): Workflow = + copy(on = Some(on)) + + def withJobs(jobs: (String, Job)*): Workflow = + copy(jobs = ListMap(jobs: _*)) + + def addJob(job: (String, Job)): Workflow = + copy(jobs = jobs + job) + + def addJobs(newJobs: (String, Job)*): Workflow = + copy(jobs = jobs ++ newJobs) +} + +object Workflow { + + implicit def listMapCodec[K: JsonFieldDecoder: JsonFieldEncoder, V: JsonCodec]: JsonCodec[ListMap[K, V]] = + JsonCodec( + JsonEncoder.keyValueIterable[K, V, ListMap], + JsonDecoder.keyValueChunk[K, V].map(c => ListMap(c: _*)) + ) + implicit lazy val codec: JsonCodec[Workflow] = DeriveJsonCodec.gen[Workflow] +} diff --git a/zio-sbt-project/build.sbt b/zio-sbt-project/build.sbt new file mode 100644 index 00000000..b2cadd2c --- /dev/null +++ b/zio-sbt-project/build.sbt @@ -0,0 +1,7 @@ +// Cross-Compiler Plugins +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.2") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") diff --git a/zio-sbt-project/src/main/scala/zio/sbt/BuildAssertions.scala b/zio-sbt-project/src/main/scala/zio/sbt/BuildAssertions.scala new file mode 100644 index 00000000..92f86048 --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/BuildAssertions.scala @@ -0,0 +1,344 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt + +import sbt.Keys._ +import sbt._ +import sbtprojectmatrix.ProjectMatrixKeys.virtualAxes + +object BuildAssertions { + + trait Keys { + + lazy val isProjectMatrix: SettingKey[Boolean] = settingKey[Boolean]("Use sbt-projectmatrix") + lazy val isScalaJS: SettingKey[Boolean] = settingKey[Boolean]("Is Scala.js project") + lazy val isScalaJVM: SettingKey[Boolean] = settingKey[Boolean]("Is JVM project") + lazy val isScalaNative: SettingKey[Boolean] = settingKey[Boolean]("Is Scala Native project") + lazy val isScala2_12: SettingKey[Boolean] = settingKey[Boolean]("Is Scala 2.12 project") + lazy val isScala2_13: SettingKey[Boolean] = settingKey[Boolean]("Is Scala 2.13 project") + lazy val isScala3: SettingKey[Boolean] = settingKey[Boolean]("Is Scala 3 project") + + lazy val isCrossbuildProject: SettingKey[Boolean] = + settingKey[Boolean]("Check if the project is a crossbuild project (crossScalaVersions.size > 1)") + + lazy val hasCrossbuildProjects: SettingKey[Boolean] = + settingKey[Boolean]("Check if the project has crossbuild projects") + } + + object Keys extends Keys + + import Keys._ + + def isProjectMatrixSetting: Setting[Boolean] = isProjectMatrix := virtualAxes.?.value.isDefined + + def requireJS[T](setting: => T): Def.Initialize[T] = + Def.settingDyn { + if (isScalaJS.value) Def.setting(setting) + else sys.error("This setting requires Scala.js") + } + + def isScalaJSSetting: Setting[Boolean] = isScalaJS := { + thisProject.?.value.map(_.autoPlugins.exists(_.label == "org.scalajs.sbtplugin.ScalaJSPlugin")).getOrElse(false) + } + + def requireJVM[T](setting: => T): Def.Initialize[T] = + Def.settingDyn { + if (isScalaJVM.value) Def.setting(setting) + else sys.error("This setting requires JVM") + } + + def isScalaJVMSetting: Setting[Boolean] = isScalaJVM := { + !isScalaJS.value && !isScalaNative.value + } + + def requireNative[T](setting: => T): Def.Initialize[T] = + Def.settingDyn { + if (isScalaNative.value) Def.setting(setting) + else sys.error("This setting requires Scala Native") + } + + def isScalaNativeSetting: Setting[Boolean] = isScalaNative := { + thisProject.?.value + .map(_.autoPlugins.exists(_.label == "scala.scalanative.sbtplugin.ScalaNativePlugin")) + .getOrElse(false) + } + + def isScala2_12Setting: Setting[Boolean] = isScala2_12 := Def.setting { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 12)) => true + case _ => false + } + }.value + + def isScala2_13Setting: Setting[Boolean] = isScala2_13 := Def.setting { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 13)) => true + case _ => false + } + }.value + + def isScala3Setting: Setting[Boolean] = isScala3 := Def.setting { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => true + case _ => false + } + }.value + + def isCrossbuildProjectSetting: Def.Setting[Boolean] = isCrossbuildProject := crossScalaVersions.value.size > 1 + + private val allAggregates = + ScopeFilter(inAggregates(ThisProject)) + + def hasCrossbuildProjectsSetting: Def.Setting[Boolean] = hasCrossbuildProjects := { + val current = isCrossbuildProject.value + current || isCrossbuildProject + .all(allAggregates) + .value + .reduce(_ || _) + } + + def contains2_12: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScala2_12.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def contains2_13: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScala2_13.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def contains3: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScala3.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJS: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJS.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJS2_12: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJS.value && isScala2_12.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJS2_13: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJS.value && isScala2_13.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJS3: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJS.value && isScala3.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJVM: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJVM.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJVM2_12: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJVM.value && isScala2_12.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJVM2_13: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJVM.value && isScala2_13.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsJVM3: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaJVM.value && isScala3.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsNative: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaNative.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsNative2_12: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaNative.value && isScala2_12.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsNative2_13: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaNative.value && isScala2_13.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def containsNative3: Def.Initialize[Boolean] = Def.settingDyn { + Def.setting { + Def.setting { + isScalaNative.value && isScala3.value + }.all(allAggregates) + .value + .reduce(_ || _) + } + } + + def scalaCrossVersionsCountsOnePerProject: Def.Initialize[Boolean] = Def.settingDyn { + val result = crossScalaVersions.?.all( + ScopeFilter( + inAggregates(LocalRootProject), + inConfigurations(Compile) + ) + ).value + Def.setting(result.map(_.forall(_.size <= 1)).reduce(_ && _)) + } + + def settings: Seq[Setting[_]] = Seq( + isScalaJSSetting, + isScalaJVMSetting, + isScalaNativeSetting, + isScala2_12Setting, + isScala2_13Setting, + isScala3Setting, + isProjectMatrixSetting, + isCrossbuildProjectSetting, + hasCrossbuildProjectsSetting, + hasCrossbuildProjects / aggregate := false + ) + + val docs: Def.Initialize[Seq[String]] = Def.settingDyn { + val containsJS2_12 = BuildAssertions.containsJS2_12.value + val containsJS2_13 = BuildAssertions.containsJS2_13.value + val containsJS3 = BuildAssertions.containsJS3.value + val containsJVM2_12 = BuildAssertions.containsJVM2_12.value + val containsJVM2_13 = BuildAssertions.containsJVM2_13.value + val containsJVM3 = BuildAssertions.containsJVM3.value + val containsNative2_12 = BuildAssertions.containsNative2_12.value + val containsNative2_13 = BuildAssertions.containsNative2_13.value + val containsNative3 = BuildAssertions.containsNative3.value + val allScalaCrossVersions = ScalaVersions.allScalaCrossVersions.value.filter(_.size > 1).flatten + val scalaCrossVersionsCountsOnePerProject = BuildAssertions.scalaCrossVersionsCountsOnePerProject.value + + val activeScala212 = + ScalaVersions.allCurrentScalaVersions.value + .filter(_.startsWith("2.12")) + .toList + .sorted + .lastOption + .getOrElse("2.12.x ") + val activeScala213 = + ScalaVersions.allCurrentScalaVersions.value + .filter(_.startsWith("2.13")) + .toList + .sorted + .lastOption + .getOrElse("2.13.x ") + val activeScala3 = + ScalaVersions.allCurrentScalaVersions.value.filter(_.startsWith("3.")).toList.sorted.lastOption.getOrElse("3.x ") + + Def.setting( + Seq( + s""" + | \u001b[33m### This build contains crossbuild projects (multiple scala versions defined in crossScalaVersions)\u001b[0m + | \u001b[33mVersions found: ${ScalaVersions.Keys.allScalaVersions.value.toSeq.sorted.mkString(", ")}\u001b + | \u001b[33mNote: having only one scala version per project makes it easier to compile and test all without `++` switching, sbt-projectmatrix can help with that.\u001b + | \u001b[33mSee: \u001b[36mhttps://github.com/sbt/sbt-projectmatrix?tab=readme-ov-file#sbt-projectmatrix\u001b + | \u001b[33mthis is feature in sbt 2.x, see: \u001b[36mhttps://www.scala-sbt.org/2.x/docs/en/reference/cross-building-setup.html#project-matrix + """.stripMargin.stripLeading() + ).filter(_ => !scalaCrossVersionsCountsOnePerProject) ++ + Seq( + // format: off + s""" + | ### Detected current Platforms, JDKs and Scala Versions ${if (!scalaCrossVersionsCountsOnePerProject) "( other builds " + (allScalaCrossVersions - scalaVersion.value).mkString(", ") + " )" else ""} + | Current JDK ${if (JavaVersions.Keys.currentJDK.value < JavaVersions.Keys.javaPlatform.value) "\u001b[31m" else ""}${JavaVersions.Keys.currentJDK.value}\u001b[0m + | Target JDK ${JavaVersions.Keys.javaPlatform.value} + | Scala | ${activeScala212} | ${activeScala213} | ${activeScala3} | + |---------|---------|---------|--------| + | JS | ${if (containsJS2_12) "✅" else "❌"} | ${if (containsJS2_13) "✅" else "❌"} | ${if (containsJS3) "✅" else "❌"} | + | JVM | ${if (containsJVM2_12) "✅" else "❌"} | ${if (containsJVM2_13) "✅" else "❌"} | ${if (containsJVM3) "✅" else "❌"} | + | Native | ${if (containsNative2_12) "✅" else "❌"} | ${if (containsNative2_13) "✅" else "❌"} | ${if (containsNative3) "✅" else "❌"} | + """.stripMargin.stripLeading() + // format: on + ) + ) + } + +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/CompileTasks.scala b/zio-sbt-project/src/main/scala/zio/sbt/CompileTasks.scala new file mode 100644 index 00000000..798e749c --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/CompileTasks.scala @@ -0,0 +1,204 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object CompileTasks { + + import BuildAssertions.Keys._ + + trait Keys { + + lazy val compile2_12: TaskKey[Unit] = taskKey[Unit]("Compile all Scala 2.12") + lazy val compile2_13: TaskKey[Unit] = taskKey[Unit]("Compile all Scala 2.13") + lazy val compile3: TaskKey[Unit] = taskKey[Unit]("Compile all Scala 3") + + lazy val compileJS: TaskKey[Unit] = taskKey[Unit]("Compile all JS projects") + lazy val compileJS2_12: TaskKey[Unit] = taskKey[Unit]("Compile all JS 2.12 projects") + lazy val compileJS2_13: TaskKey[Unit] = taskKey[Unit]("Compile all JS 2.13 projects") + lazy val compileJS3: TaskKey[Unit] = taskKey[Unit]("Compile all JS 3 projects") + + lazy val compileJVM: TaskKey[Unit] = taskKey[Unit]("Compile all the JVM projects") + lazy val compileJVM2_12: TaskKey[Unit] = taskKey[Unit]("Compile all JVM 2.12 projects") + lazy val compileJVM2_13: TaskKey[Unit] = taskKey[Unit]("Compile all JVM 2.13 projects") + lazy val compileJVM3: TaskKey[Unit] = taskKey[Unit]("Compile all JVM 3 projects") + + lazy val compileNative: TaskKey[Unit] = taskKey[Unit]("Compile all Native projects") + lazy val compileNative2_12: TaskKey[Unit] = taskKey[Unit]("Compile all Native 2.12 projects") + lazy val compileNative2_13: TaskKey[Unit] = taskKey[Unit]("Compile all Native 2.13 projects") + lazy val compileNative3: TaskKey[Unit] = taskKey[Unit]("Compile all Native 3 projects") + + } + + object Keys extends Keys + + import Keys._ + + def compileOn( + condition: Def.Initialize[Boolean], + label: Option[Def.Initialize[Task[String]]] = None + ): Def.Initialize[Task[Unit]] = + Def.taskIf { + if (condition.value) { + val name = label.getOrElse(Def.task("")).value + if (name.nonEmpty) sLog.value.info("Compiling " + name) + val _ = (Scope.ThisScope / compile).value + } else () + } + + def compile2_12Task: Def.Setting[Task[Unit]] = compile2_12 := compileOn( + isScala2_12, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def compile2_13Task: Def.Setting[Task[Unit]] = compile2_13 := compileOn( + isScala2_13, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def compile3Task: Def.Setting[Task[Unit]] = compile3 := compileOn( + isScala3, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def compileJSTask: Def.Setting[Task[Unit]] = + compileJS := compileOn(isScalaJS, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def compileJS2_12Task: Def.Setting[Task[Unit]] = compileJS2_12 := compileOn( + isScalaJS.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileJS2_13Task: Def.Setting[Task[Unit]] = compileJS2_13 := compileOn( + isScalaJS.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileJS3Task: Def.Setting[Task[Unit]] = compileJS3 := compileOn( + isScalaJS.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def compileJVMTask: Def.Setting[Task[Unit]] = + compileJVM := compileOn(isScalaJVM, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def compileJVM2_12Task: Def.Setting[Task[Unit]] = compileJVM2_12 := compileOn( + isScalaJVM.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileJVM2_13Task: Def.Setting[Task[Unit]] = compileJVM2_13 := compileOn( + isScalaJVM.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileJVM3Task: Def.Setting[Task[Unit]] = compileJVM3 := compileOn( + isScalaJVM.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def compileNativeTask: Def.Setting[Task[Unit]] = + compileNative := compileOn(isScalaNative, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def compileNative2_12Task: Def.Setting[Task[Unit]] = compileNative2_12 := compileOn( + isScalaNative.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileNative2_13Task: Def.Setting[Task[Unit]] = compileNative2_13 := compileOn( + isScalaNative.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def compileNative3Task: Def.Setting[Task[Unit]] = compileNative3 := compileOn( + isScalaNative.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def settings: Seq[Setting[Task[Unit]]] = Seq( + compile2_12Task, + compile2_13Task, + compile3Task, + compileJSTask, + compileJS2_12Task, + compileJS2_13Task, + compileJS3Task, + compileJVMTask, + compileJVM2_12Task, + compileJVM2_13Task, + compileJVM3Task, + compileNativeTask, + compileNative2_12Task, + compileNative2_13Task, + compileNative3Task + ) + + val docs: Def.Initialize[Seq[(String, String)]] = Def.settingDyn { + val contains2_12 = BuildAssertions.contains2_12.value + val contains2_13 = BuildAssertions.contains2_13.value + val contains3 = BuildAssertions.contains3.value + val containsJS = BuildAssertions.containsJS.value + val containsJS2_12 = BuildAssertions.containsJS2_12.value + val containsJS2_13 = BuildAssertions.containsJS2_13.value + val containsJS3 = BuildAssertions.containsJS3.value + val containsJVM = BuildAssertions.containsJVM.value + val containsJVM2_12 = BuildAssertions.containsJVM2_12.value + val containsJVM2_13 = BuildAssertions.containsJVM2_13.value + val containsJVM3 = BuildAssertions.containsJVM3.value + val containsNative = BuildAssertions.containsNative.value + val containsNative2_12 = BuildAssertions.containsNative2_12.value + val containsNative2_13 = BuildAssertions.containsNative2_13.value + val containsNative3 = BuildAssertions.containsNative3.value + + Def.setting( + Seq( + Seq("compile" -> "Compile all projects"), + if (contains2_12) Seq(compile2_12Task.key.key.label -> compile2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (contains2_13) Seq(compile2_13Task.key.key.label -> compile2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (contains3) Seq(compile3Task.key.key.label -> compile3Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS) Seq(compileJSTask.key.key.label -> compileJSTask.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS2_12) Seq(compileJS2_12Task.key.key.label -> compileJS2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS2_13) Seq(compileJS2_13Task.key.key.label -> compileJS2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS3) Seq(compileJS3Task.key.key.label -> compileJS3Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM) + Seq(compileJVMTask.key.key.label -> compileJVMTask.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM2_12) + Seq(compileJVM2_12Task.key.key.label -> compileJVM2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM2_13) + Seq(compileJVM2_13Task.key.key.label -> compileJVM2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM3) Seq(compileJVM3Task.key.key.label -> compileJVM3Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative) + Seq(compileNativeTask.key.key.label -> compileNativeTask.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative2_12) + Seq(compileNative2_12Task.key.key.label -> compileNative2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative2_13) + Seq(compileNative2_13Task.key.key.label -> compileNative2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative3) + Seq(compileNative3Task.key.key.label -> compileNative3Task.key.key.description.getOrElse("")) + else Seq.empty + ).flatten + ) + } + +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/JavaVersions.scala b/zio-sbt-project/src/main/scala/zio/sbt/JavaVersions.scala new file mode 100644 index 00000000..e06fea38 --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/JavaVersions.scala @@ -0,0 +1,43 @@ +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object JavaVersion { + + val `11` = "11" + val `17` = "17" + val `21` = "21" +} + +object JavaVersions { + + trait Keys { + + lazy val javaPlatform: SettingKey[String] = settingKey[String]("java target platform, default is 11") + lazy val currentJDK: SettingKey[String] = settingKey[String]("current JDK version") + + } + + object Keys extends Keys + + import Keys._ + + def buildSettings: Seq[Setting[_]] = Seq( + javaPlatform := { + val targetJVM = javaPlatform.?.value.getOrElse(JavaVersion.`11`) + val jdkNeedsUpgrade = currentJDK.value.toInt < targetJVM.toInt + if (jdkNeedsUpgrade) + sLog.value.warn( + s"\u001b[33mJDK upgrade is required, target ($targetJVM) is higher than the current JDK version (${currentJDK.value}), compilation will fail!\u001b" + ) + targetJVM + }, + scalacOptions ++= Seq(s"-release:${javaPlatform.value}"), + javacOptions ++= Seq("-source", javaPlatform.value, "-target", javaPlatform.value) + ) + + def globalSettings: Seq[Setting[_]] = Seq( + currentJDK := sys.props("java.specification.version") + ) +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/ScalaPlatform.scala b/zio-sbt-project/src/main/scala/zio/sbt/ScalaPlatform.scala new file mode 100644 index 00000000..8ccc9e2c --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/ScalaPlatform.scala @@ -0,0 +1,53 @@ +package zio.sbt + +import sbt._ + +sealed trait ScalaPlatform { + def asString: String = this match { + case ScalaPlatform.JS => "JS" + case ScalaPlatform.JVM => "JVM" + case ScalaPlatform.Native => "Native" + } +} +object ScalaPlatform { + case object JS extends ScalaPlatform + case object JVM extends ScalaPlatform + case object Native extends ScalaPlatform +} + +object ScalaPlatforms { + + trait Keys { + + lazy val allScalaPlatforms: SettingKey[Set[ScalaPlatform]] = + settingKey[Set[ScalaPlatform]]("All Scala platforms used in the whole project") + + } + + object Keys extends Keys + + import Keys._ + + def allScalaPlatformsSetting: Def.Setting[Set[ScalaPlatform]] = allScalaPlatforms := { + Def.setting { + Set( + BuildAssertions.Keys.isScalaJS.value -> ScalaPlatform.JS, + BuildAssertions.Keys.isScalaJVM.value -> ScalaPlatform.JVM, + BuildAssertions.Keys.isScalaNative.value -> ScalaPlatform.Native + ).collect { case (true, p) => + p + } + }.all( + ScopeFilter( + inAggregates(LocalRootProject), + inConfigurations(Compile) + ) + ).value + .flatten + .toSet + } + + def globalSettings: Seq[Setting[_]] = Seq( + allScalaPlatformsSetting + ) +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/ScalaVersions.scala b/zio-sbt-project/src/main/scala/zio/sbt/ScalaVersions.scala new file mode 100644 index 00000000..608cdbbd --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/ScalaVersions.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object ScalaVersion { + val Scala3 = "3.3.4" + val Scala212 = "2.12.20" + val Scala213 = "2.13.15" +} + +object ScalaVersions { + + trait Keys { + + lazy val scala212: SettingKey[String] = settingKey[String]("Scala 2.12 version") + lazy val scala213: SettingKey[String] = settingKey[String]("Scala 2.13 version") + lazy val scala3: SettingKey[String] = settingKey[String]("Scala 3 version") + + lazy val defaultCrossScalaVersions: SettingKey[Seq[String]] = + settingKey[Seq[String]]("Optional default cross scala versions") + lazy val allScalaVersions: SettingKey[Set[String]] = + settingKey[Set[String]]("All scala versions used in the whole project") + + } + + object Keys extends Keys + + import Keys._ + + def allCurrentScalaVersions: Def.Initialize[Set[String]] = Def.setting { + scalaVersion.?.all( + ScopeFilter( + inAggregates(LocalRootProject), + inConfigurations(Compile) + ) + ).value.flatten.toSet + } + + def allScalaVersionsSetting: Def.Setting[Set[String]] = allScalaVersions := { + allScalaCrossVersions.value.flatten ++ allCurrentScalaVersions.value + } + + def allScalaCrossVersions: Def.Initialize[Set[Set[String]]] = Def.setting { + crossScalaVersions.?.all( + ScopeFilter( + inAggregates(LocalRootProject), + inConfigurations(Compile) + ) + ).value.map(_.map(_.toSet)).flatten.toSet + } + + def buildSettings: Seq[Setting[_]] = Seq( + scala212 := ScalaVersion.Scala212, + scala213 := ScalaVersion.Scala213, + scala3 := ScalaVersion.Scala3 + ) + + def globalSettings: Seq[Setting[_]] = Seq( + allScalaVersionsSetting + ) + +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/TestTasks.scala b/zio-sbt-project/src/main/scala/zio/sbt/TestTasks.scala new file mode 100644 index 00000000..44a739b7 --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/TestTasks.scala @@ -0,0 +1,210 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object TestTasks { + + import BuildAssertions.Keys._ + + trait Keys { + + lazy val test2_12: TaskKey[Unit] = taskKey[Unit]("Test all Scala 2.12") + lazy val test2_13: TaskKey[Unit] = taskKey[Unit]("Test all Scala 2.13") + lazy val test3: TaskKey[Unit] = taskKey[Unit]("Test all Scala 3") + + lazy val testJS: TaskKey[Unit] = taskKey[Unit]("Test all JS") + lazy val testJS2_12: TaskKey[Unit] = taskKey[Unit]("Test all JS 2.12") + lazy val testJS2_13: TaskKey[Unit] = taskKey[Unit]("Test all JS 2.13") + lazy val testJS3: TaskKey[Unit] = taskKey[Unit]("Test all JS 3") + + lazy val testJVM: TaskKey[Unit] = taskKey[Unit]("Test all the JVM") + lazy val testJVM2_12: TaskKey[Unit] = taskKey[Unit]("Test all JVM 2.12") + lazy val testJVM2_13: TaskKey[Unit] = taskKey[Unit]("Test all JVM 2.13") + lazy val testJVM3: TaskKey[Unit] = taskKey[Unit]("Test all JVM 3") + + lazy val testNative: TaskKey[Unit] = taskKey[Unit]("Test all Native") + lazy val testNative2_12: TaskKey[Unit] = taskKey[Unit]("Test all Native 2.12") + lazy val testNative2_13: TaskKey[Unit] = taskKey[Unit]("Test all Native 2.13") + lazy val testNative3: TaskKey[Unit] = taskKey[Unit]("Test all Native 3") + + } + + object Keys extends Keys + + def scalaVersionToSuffix(scalaVersion: String): String = + CrossVersion.partialVersion(scalaVersion) match { + case Some((3, _)) => "3" + case Some((2, 13)) => "2_13" + case Some((2, 12)) => "2_12" + case _ => throw new Exception(s"Unsupported Scala version: $scalaVersion") + } + + import Keys._ + + def testOn( + condition: Def.Initialize[Boolean], + label: Option[Def.Initialize[Task[String]]] = None + ): Def.Initialize[Task[Unit]] = + Def.taskIf { + if (condition.value) { + val name = label.getOrElse(Def.task("")).value + + if (name.nonEmpty) sLog.value.info("Running tests for " + name) + + test.in(Scope.ThisScope).value + } else () + } + + def test2_12Task: Def.Setting[Task[Unit]] = test2_12 := testOn( + isScala2_12, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def test2_13Task: Def.Setting[Task[Unit]] = test2_13 := testOn( + isScala2_13, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def test3Task: Def.Setting[Task[Unit]] = test3 := testOn( + isScala3, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def testJSTask: Def.Setting[Task[Unit]] = + testJS := testOn(isScalaJS, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def testJS2_12Task: Def.Setting[Task[Unit]] = testJS2_12 := testOn( + isScalaJS.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testJS2_13Task: Def.Setting[Task[Unit]] = testJS2_13 := testOn( + isScalaJS.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testJS3Task: Def.Setting[Task[Unit]] = testJS3 := testOn( + isScalaJS.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def testJVMTask: Def.Setting[Task[Unit]] = + testJVM := testOn(isScalaJVM, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def testJVM2_12Task: Def.Setting[Task[Unit]] = testJVM2_12 := testOn( + isScalaJVM.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testJVM2_13Task: Def.Setting[Task[Unit]] = testJVM2_13 := testOn( + isScalaJVM.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testJVM3Task: Def.Setting[Task[Unit]] = testJVM3 := testOn( + isScalaJVM.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def testNativeTask: Def.Setting[Task[Unit]] = + testNative := testOn(isScalaNative, Some(thisProject.?.map(_.map(_.id).getOrElse("")))).value + def testNative2_12Task: Def.Setting[Task[Unit]] = testNative2_12 := testOn( + isScalaNative.zip(isScala2_12) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testNative2_13Task: Def.Setting[Task[Unit]] = testNative2_13 := testOn( + isScalaNative.zip(isScala2_13) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + def testNative3Task: Def.Setting[Task[Unit]] = testNative3 := testOn( + isScalaNative.zip(isScala3) { case (a, b) => a && b }, + Some(thisProject.?.map(_.map(_.id).getOrElse(""))) + ).value + + def settings: Seq[Setting[Task[Unit]]] = Seq( + test2_12Task, + test2_13Task, + test3Task, + testJSTask, + testJS2_12Task, + testJS2_13Task, + testJS3Task, + testJVMTask, + testJVM2_12Task, + testJVM2_13Task, + testJVM3Task, + testNativeTask, + testNative2_12Task, + testNative2_13Task, + testNative3Task + ) + + val docs: Def.Initialize[Seq[(String, String)]] = Def.settingDyn { + + val contains2_12 = BuildAssertions.contains2_12.value + val contains2_13 = BuildAssertions.contains2_13.value + val contains3 = BuildAssertions.contains3.value + val containsJS = BuildAssertions.containsJS.value + val containsJS2_12 = BuildAssertions.containsJS2_12.value + val containsJS2_13 = BuildAssertions.containsJS2_13.value + val containsJS3 = BuildAssertions.containsJS3.value + val containsJVM = BuildAssertions.containsJVM.value + val containsJVM2_12 = BuildAssertions.containsJVM2_12.value + val containsJVM2_13 = BuildAssertions.containsJVM2_13.value + val containsJVM3 = BuildAssertions.containsJVM3.value + val containsNative = BuildAssertions.containsNative.value + val containsNative2_12 = BuildAssertions.containsNative2_12.value + val containsNative2_13 = BuildAssertions.containsNative2_13.value + val containsNative3 = BuildAssertions.containsNative3.value + + Def.setting( + Seq("test" -> "Test all projects") ++ + Seq( + if (contains2_12) Seq(test2_12Task.key.key.label -> test2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (contains2_13) Seq(test2_13Task.key.key.label -> test2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (contains3) Seq(test3Task.key.key.label -> test3Task.key.key.description.getOrElse("")) else Seq.empty, + if (containsJS) Seq(testJSTask.key.key.label -> testJSTask.key.key.description.getOrElse("")) else Seq.empty, + if (containsJS2_12) Seq(testJS2_12Task.key.key.label -> testJS2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS2_13) Seq(testJS2_13Task.key.key.label -> testJS2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJS3) Seq(testJS3Task.key.key.label -> testJS3Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM) Seq(testJVMTask.key.key.label -> testJVMTask.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM2_12) Seq(testJVM2_12Task.key.key.label -> testJVM2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM2_13) Seq(testJVM2_13Task.key.key.label -> testJVM2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsJVM3) Seq(testJVM3Task.key.key.label -> testJVM3Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative) Seq(testNativeTask.key.key.label -> testNativeTask.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative2_12) + Seq(testNative2_12Task.key.key.label -> testNative2_12Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative2_13) + Seq(testNative2_13Task.key.key.label -> testNative2_13Task.key.key.description.getOrElse("")) + else Seq.empty, + if (containsNative3) Seq(testNative3Task.key.key.label -> testNative3Task.key.key.description.getOrElse("")) + else Seq.empty + ).flatten + ++ Seq( + """testOnly *.YourSpec -- -t \"YourLabel\"""" -> "Only runs tests with matching term e.g." + ) + ) + } +} diff --git a/zio-sbt-project/src/main/scala/zio/sbt/ZioSbtCrossbuildPlugin.scala b/zio-sbt-project/src/main/scala/zio/sbt/ZioSbtCrossbuildPlugin.scala new file mode 100644 index 00000000..9d1935ca --- /dev/null +++ b/zio-sbt-project/src/main/scala/zio/sbt/ZioSbtCrossbuildPlugin.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2022-2023 dev.zio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zio.sbt + +import sbt.{JavaVersion => _, _} + +import zio.sbt.ZioSbtShared.autoImport.{banners, usefulTasksAndSettings} + +object ZioSbtCrossbuildPlugin extends AutoPlugin { + + override def trigger = allRequirements + + override def requires: Plugins = super.requires && ZioSbtShared + + object autoImport + extends BuildAssertions.Keys + with CompileTasks.Keys + with TestTasks.Keys + with ScalaVersions.Keys + with ScalaPlatforms.Keys + with JavaVersions.Keys { + + def zioSbtCrossbuildSettings: Seq[Setting[_]] = + BuildAssertions.settings ++ + inConfig(Compile)(CompileTasks.settings) ++ + inConfig(Test)(CompileTasks.settings) ++ + inConfig(Test)(TestTasks.settings) + } + + import autoImport.* + + override def projectSettings: Seq[Setting[_]] = zioSbtCrossbuildSettings ++ Seq( + banners ++= { + if (SharedTasks.isRoot.value) BuildAssertions.docs.value + else Seq.empty + }, + usefulTasksAndSettings ++= { + if (SharedTasks.isRoot.value) { + CompileTasks.docs.value ++ TestTasks.docs.value + } else Seq.empty + } + ) + + override def buildSettings: Seq[Def.Setting[_]] = + super.buildSettings ++ + ScalaVersions.buildSettings ++ + JavaVersions.buildSettings + + override def globalSettings: Seq[Def.Setting[_]] = + super.globalSettings ++ + ScalaVersions.globalSettings ++ + ScalaPlatforms.globalSettings ++ + JavaVersions.globalSettings +} diff --git a/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasks.scala b/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasks.scala new file mode 100644 index 00000000..d8bf3e51 --- /dev/null +++ b/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasks.scala @@ -0,0 +1,12 @@ +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object SharedTasks { + + val isRoot: Def.Initialize[Boolean] = Def.settingDyn { + val r = thisProjectRef.?.value.fold(true)(_.project == loadedBuild.value.units(loadedBuild.value.root).root) + Def.setting(r) + } +} diff --git a/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasksAndSettings.scala b/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasksAndSettings.scala new file mode 100644 index 00000000..1d142175 --- /dev/null +++ b/zio-sbt-shared/src/main/scala/zio/sbt/SharedTasksAndSettings.scala @@ -0,0 +1,100 @@ +package zio.sbt + +import sbt.Keys._ +import sbt._ + +object ZioSbtShared extends AutoPlugin { + + override def trigger = allRequirements + + override def requires: Plugins = super.requires + + object autoImport { + + val welcomeBannerEnabled: SettingKey[Boolean] = + settingKey[Boolean]("Indicates whether or not to enable the welcome banner.") + + val banners: SettingKey[Seq[String]] = settingKey[Seq[String]]( + "A list of banners that will be displayed as part of the welcome message." + ) + + val welcomeTaskAndSettingsEnabled: SettingKey[Boolean] = + settingKey[Boolean]("Indicates whether or not to enable the welcome task and settings.") + + val usefulTasksAndSettings: SettingKey[Seq[(String, String)]] = settingKey[Seq[(String, String)]]( + "A map of useful tasks and settings that will be displayed as part of the welcome banner." + ) + + val welcomeMessage: Setting[String] = ZioSbtShared.welcomeMessageSetting + } + + import autoImport.* + + private val allAggregates = + ScopeFilter(inAggregates(ThisProject)) + + val allBanners: Def.Initialize[Seq[String]] = Def.settingDyn { + Def.setting( + banners + .all(allAggregates) + .value + .foldLeft(Seq.empty[String])(_ ++ _) + ) + } + + val allUsefulTasksAndSettings: Def.Initialize[Seq[(String, String)]] = + Def.settingDyn { + Def.setting( + usefulTasksAndSettings + .all(allAggregates) + .value + .foldLeft(Seq.empty[(String, String)])(_ ++ _) + ) + } + + def welcomeMessageSetting: Setting[String] = + onLoadMessage := { + if (welcomeBannerEnabled.value) { + import scala.Console + + val allBanners = ZioSbtShared.allBanners.value + val allUsefulTasksAndSettings = ZioSbtShared.allUsefulTasksAndSettings.value + val maxLen = (allUsefulTasksAndSettings.map(_._1.length) ++ Seq(1)).max + + def normalizedPadding(s: String) = " " * (maxLen - s.length) + + def item(text: String): String = s"${Console.GREEN}> ${Console.CYAN}$text${Console.RESET}" + + val renderedBanners = allBanners.mkString("\n") + val renderedTasksAndSettings = + if (welcomeTaskAndSettingsEnabled.value) { + s"""|Useful sbt tasks: + |${allUsefulTasksAndSettings.map { case (task, description) => + s"${item(task)} ${normalizedPadding(task)}${description}" + } + .mkString("\n")} + """.stripMargin + } else "" + + s"""| + |$renderedBanners + |$renderedTasksAndSettings + |""".stripMargin + } else "" + } + + override def projectSettings: Seq[Setting[_]] = Seq( + banners := Seq.empty, + usefulTasksAndSettings := Seq.empty + ) + + override def globalSettings: Seq[Def.Setting[_]] = super.globalSettings ++ + Seq( + welcomeBannerEnabled := true, + welcomeTaskAndSettingsEnabled := true, + banners := Seq.empty, + usefulTasksAndSettings := Seq.empty, + Global / excludeLintKeys += usefulTasksAndSettings + ) + +} diff --git a/zio-sbt-website/build.sbt b/zio-sbt-website/build.sbt index f45bd37d..0e500884 100644 --- a/zio-sbt-website/build.sbt +++ b/zio-sbt-website/build.sbt @@ -1,6 +1,7 @@ -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.2") -libraryDependencies += "dev.zio" %% "zio" % "2.1.8" -libraryDependencies += "io.circe" %% "circe-yaml" % "0.16.0" +libraryDependencies += "dev.zio" %% "zio" % "2.1.11" +libraryDependencies += "dev.zio" %% "zio-json" % "0.7.3" +libraryDependencies += "dev.zio" %% "zio-json-yaml" % "0.7.3" diff --git a/zio-sbt-website/src/sbt-test/zio-sbt-website/compileDocs/build.sbt b/zio-sbt-website/src/sbt-test/zio-sbt-website/compileDocs/build.sbt index b7765627..52ed94a2 100644 --- a/zio-sbt-website/src/sbt-test/zio-sbt-website/compileDocs/build.sbt +++ b/zio-sbt-website/src/sbt-test/zio-sbt-website/compileDocs/build.sbt @@ -3,6 +3,8 @@ lazy val root = (project in file(".")) version := "0.1", projectName := "ZIO SBT", mainModuleName := "test-project", - projectStage := ProjectStage.ProductionReady + projectStage := ProjectStage.ProductionReady, + docsVersion := version.value, + publishToNpm := {} ) .enablePlugins(WebsitePlugin) diff --git a/zio-sbt-website/src/sbt-test/zio-sbt-website/installWebsite/build.sbt b/zio-sbt-website/src/sbt-test/zio-sbt-website/installWebsite/build.sbt index b7765627..52ed94a2 100644 --- a/zio-sbt-website/src/sbt-test/zio-sbt-website/installWebsite/build.sbt +++ b/zio-sbt-website/src/sbt-test/zio-sbt-website/installWebsite/build.sbt @@ -3,6 +3,8 @@ lazy val root = (project in file(".")) version := "0.1", projectName := "ZIO SBT", mainModuleName := "test-project", - projectStage := ProjectStage.ProductionReady + projectStage := ProjectStage.ProductionReady, + docsVersion := version.value, + publishToNpm := {} ) .enablePlugins(WebsitePlugin)