Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 139 additions & 62 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,6 @@ on:
`clean` on sbt 1, detected from project/build.properties.
type: string
default: ''
coverage:
description: 'Collect test coverage and upload it to Coveralls'
type: boolean
default: true
version_policy_check:
description: 'Run versionPolicyCheck (binary compatibility). Requires sbt-version-policy.'
type: boolean
default: true
scalafmt_check:
description: 'Check formatting with scalafmt'
type: boolean
default: true
doc_check:
description: 'Check that Scaladoc builds'
type: boolean
default: true
sonar:
description: >-
Run a SonarQube Cloud scan. Requires a SONAR_TOKEN secret and an existing project on
Expand All @@ -69,36 +53,25 @@ concurrency:
cancel-in-progress: true

jobs:
test:
scala-versions:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
scala: ${{ fromJSON(inputs.scala_versions) }}

steps:
- name: checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Define Scala versions
id: scala
run: echo 'versions=${{ inputs.scala_versions }}' >> "$GITHUB_OUTPUT"

- uses: coursier/cache-action@v8
outputs:
version: ${{ steps.scala.outputs.versions }}

- name: setup Java ${{ inputs.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ inputs.java_version }}
distribution: ${{ inputs.java_distribution }}
cache: 'sbt'
sbt-tasks:
runs-on: ubuntu-latest

- name: setup SBT
uses: sbt/setup-sbt@v1
steps:
- name: checkout
uses: actions/checkout@v7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
# sbt 2's disk cache is restored across runs and is not keyed on scoverage's
# instrumentation. A cached compile would be reused without re-emitting coverage data,
# leaving an empty report while the build still passes.
disk-cache: ${{ !inputs.coverage }}
persist-credentials: false

- name: resolve test task
id: test-task
Expand Down Expand Up @@ -128,24 +101,53 @@ jobs:
echo "clean=clean" >> "$GITHUB_OUTPUT"
fi

outputs:
test: ${{ steps.test-task.outputs.test }}
clean: ${{ steps.clean-task.outputs.clean }}

test-coverage:
runs-on: ubuntu-latest
needs: [scala-versions, sbt-tasks]
strategy:
matrix:
scala: ${{ fromJSON(needs.scala-versions.outputs.version) }}

steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: restore cache
uses: coursier/cache-action@v8

- name: setup Java ${{ inputs.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ inputs.java_version }}
distribution: ${{ inputs.java_distribution }}
cache: 'sbt'

- name: setup SBT
uses: sbt/setup-sbt@v1
with:
# sbt 2's disk cache is restored across runs. Disable disk cache to force full coverage run
disk-cache: false

# The coverage build runs before any other compile: scoverage's instrumentation is not part of
# sbt's compile cache key, so a plain compile done first would be reused here and the coverage
# report would come out empty.
- name: build ${{ matrix.scala }}
run: |
if [[ "${{ inputs.coverage }}" == "true" ]]; then
sbt "++${{ matrix.scala }}; ${{ steps.clean-task.outputs.clean }}; coverage; ${{ steps.test-task.outputs.test }}; coverageAggregate"
else
sbt "++${{ matrix.scala }}; ${{ steps.clean-task.outputs.clean }}; ${{ steps.test-task.outputs.test }}"
fi
sbt "++${{ matrix.scala }}; ${{ needs.sbt-tasks.outputs.clean }}; coverage; ${{ needs.sbt-tasks.outputs.test }}; coverageAggregate"

- name: locate coverage report
id: coverage
if: inputs.coverage && success()
if: success()
run: echo "file=$(find . -path '*/coverage-report/cobertura.xml' | head -1)" >> "$GITHUB_OUTPUT"

- name: fail if coverage report is empty
if: inputs.coverage && success()
if: success()
env:
REPORT: ${{ steps.coverage.outputs.file }}
run: |
Expand All @@ -162,27 +164,13 @@ jobs:
fi

- name: upload coverage
if: inputs.coverage && success()
if: success()
uses: coverallsapp/github-action@v2
with:
file: ${{ steps.coverage.outputs.file }}
format: cobertura
flag-name: Scala ${{ matrix.scala }}

# These run as explicit tasks rather than through a project-local `check` alias, which can be
# stubbed out and then silently guarantees nothing.
- name: binary compatibility ${{ matrix.scala }}
if: inputs.version_policy_check
run: sbt "++${{ matrix.scala }}; versionPolicyCheck"

- name: formatting ${{ matrix.scala }}
if: inputs.scalafmt_check
run: sbt "++${{ matrix.scala }}; scalafmtCheckAll; scalafmtSbtCheck"

- name: scaladoc ${{ matrix.scala }}
if: inputs.doc_check
run: sbt "++${{ matrix.scala }}; Compile/doc"

# Scanned once, on the first Scala version only: SonarQube tracks one analysis per branch, so
# running it per matrix leg would have the legs overwrite each other.
- name: sonar settings
Expand All @@ -201,7 +189,7 @@ jobs:

- name: sonar scan
if: inputs.sonar && matrix.scala == fromJSON(inputs.scala_versions)[0]
uses: SonarSource/sonarqube-scan-action@v8.2
uses: SonarSource/sonarqube-scan-action@v8
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
Expand All @@ -210,3 +198,92 @@ jobs:
-Dsonar.projectKey=${{ steps.sonar.outputs.key }}
-Dsonar.scala.coverage.reportPaths=${{ steps.sonar.outputs.reports }}
${{ inputs.sonar_args }}

binary-compatibility:
runs-on: ubuntu-latest
needs: [scala-versions, sbt-tasks]
strategy:
matrix:
scala: ${{ fromJSON(needs.scala-versions.outputs.version) }}

steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false
# checkout whole history with all tags - required by `versionPolicyCheck` for version comparison
fetch-depth: 0

- name: restore cache
uses: coursier/cache-action@v8

- name: setup Java ${{ inputs.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ inputs.java_version }}
distribution: ${{ inputs.java_distribution }}
cache: 'sbt'

- name: setup SBT
uses: sbt/setup-sbt@v1

- name: binary compatibility ${{ matrix.scala }}
run: sbt "++${{ matrix.scala }}; versionPolicyCheck"

formatting:
runs-on: ubuntu-latest
needs: [scala-versions, sbt-tasks]
strategy:
matrix:
scala: ${{ fromJSON(needs.scala-versions.outputs.version) }}

steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: restore cache
uses: coursier/cache-action@v8

- name: setup Java ${{ inputs.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ inputs.java_version }}
distribution: ${{ inputs.java_distribution }}
cache: 'sbt'

- name: setup SBT
uses: sbt/setup-sbt@v1

- name: formatting ${{ matrix.scala }}
run: sbt "++${{ matrix.scala }}; scalafmtCheckRepo"

scaladoc:
runs-on: ubuntu-latest
needs: [scala-versions, sbt-tasks]
strategy:
matrix:
scala: ${{ fromJSON(needs.scala-versions.outputs.version) }}

steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: restore cache
uses: coursier/cache-action@v8

- name: setup Java ${{ inputs.java_version }}
uses: actions/setup-java@v5
with:
java-version: ${{ inputs.java_version }}
distribution: ${{ inputs.java_distribution }}
cache: 'sbt'

- name: setup SBT
uses: sbt/setup-sbt@v1

- name: scaladoc ${{ matrix.scala }}
run: sbt "++${{ matrix.scala }}; ${{ needs.sbt-tasks.outputs.clean }}; Compile/doc"
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:
with:
fetch-depth: 0

- uses: coursier/cache-action@v8
- name: restore cache
uses: coursier/cache-action@v8

- name: setup Java 17
uses: actions/setup-java@v5
Expand Down
61 changes: 26 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Scala GitHub Actions

## Scala CI workflow
## Scala Continuous Integration (CI) workflow

Runs tests, coverage, binary compatibility, formatting and Scaladoc on every push and pull request.
Replaces the handwritten `ci.yml` that each project used to carry.
Runs tests with coverage, binary compatibility, formatting and Scaladoc checks on every pull request and push on
"main" branch. Replaces the handwritten `ci.yml` that each project used to carry.

### Setup

Expand All @@ -19,7 +19,7 @@ on:

jobs:
test:
uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@<sha> # v6.2.0
uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@<sha> # v6.4.0
```

Nothing else is required if the project uses the defaults below.
Expand Down Expand Up @@ -48,10 +48,6 @@ alternatives and drops the security rating to C:
| `java_version` | `'17'` | |
| `java_distribution` | `'temurin'` | |
| `test_task` | auto | `testFull` on sbt 2, `test` on sbt 1, read from `project/build.properties` |
| `coverage` | `true` | collect coverage and upload to Coveralls |
| `version_policy_check` | `true` | requires [sbt-version-policy](https://github.com/scalacenter/sbt-version-policy/) |
| `scalafmt_check` | `true` | |
| `doc_check` | `true` | runs `Compile/doc` |
| `sonar` | `false` | run a SonarQube Cloud scan, see below |
| `sonar_project_key` | `<owner>_<repo>` | |
| `sonar_args` | `''` | extra `-D` arguments for the scanner |
Expand All @@ -61,33 +57,28 @@ Example for a project without `sbt-version-policy` and on a different Scala set:
```yaml
jobs:
test:
uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@<sha> # v6.2.0
uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@<sha> # v6.4.0
with:
scala_versions: '["2.13.18", "3.3.7"]'
version_policy_check: false
```

### Why the steps are ordered this way
### jobs in CI pipeline

Two sbt 2 behaviours make a naive coverage setup report nothing while still passing:
All checks are run concurrently! Ideally, we must strive to keep them all green, but, it is allowed to merge PR, if
some checks are red, for example if code formatting is not introduced, yet. Such red checks must be treated as nudge
to improve the quality of code in repo!
Comment thread
coderabbitai[bot] marked this conversation as resolved.

* sbt 2's compile cache is **not** keyed on scoverage's instrumentation. If a plain compile runs
first, the coverage build reuses those uninstrumented classes and the report comes out empty. The
coverage build therefore runs **before** the formatting, binary-compatibility and scaladoc checks.
* `sbt/setup-sbt` restores `~/.cache/sbt` across runs by default, which reintroduces the same problem
on any run whose build files did not change. This workflow sets `disk-cache: false` whenever
coverage is enabled.

The workflow also fails if the produced Cobertura report has no valid lines, so a silently empty
report is an error rather than a green build.

Binary compatibility, formatting and Scaladoc run as **explicit sbt tasks**, not via a project-local
`check` alias. An alias can be stubbed out (`addCommandAlias("check", "show version")`), which makes
the gate silently guarantee nothing.

The workflow checks out with `fetch-depth: 0`. Without tags sbt-dynver reports the version as
`0.0.0`, `versionPolicyCheck` then has no previous version to compare against, and the binary
compatibility check passes without checking anything.
* `test-coverage` - runs with disabled disk cache for SBT setup action (`disk-cache: false`) to make sure that
test coverage gets run with fully instrumented compilation. The workflow also fails if the produced Cobertura
report has no valid lines, so a silently empty report is an error rather than a green build.
If project has `sonar` integration configured and
enabled, then `sonar scan` will get run after coverage reports are uploaded
* `binary-compatibility` - runs [sbt-version-policy](https://github.com/scalacenter/sbt-version-policy/)'s
`versionPolicyCheck` task on repo with full history (`fetch-depth: 0`) to make sure that plugin can find the tag
for previous version
* `formatting` - runs [scalafmt](https://scalameta.org/scalafmt/)'s `scalafmtCheckRepo` task (requires at
least version 2.6.2)
* `scaladoc` - calls `Compile/doc` task to make sure that Scaladocs compile

### SonarQube Cloud

Expand Down Expand Up @@ -128,13 +119,13 @@ being analysed should have the app removed rather than left in that state.
To use Scala Release workflow have to set up project:
* add latest [sbt-dynver](https://github.com/sbt/sbt-dynver) plugin
* add Evolution's artifactory plugin [sbt-artifactory-plugin](https://github.com/evolution-gaming/sbt-artifactory-plugin)
* defined command alias `check` which runs code quality checks, for example: [scalafmt](https://scalameta.org/scalafmt/)
and [scalafix](https://scalacenter.github.io/scalafix/), and binary compatibility check by
[sbt-version-policy](https://github.com/scalacenter/sbt-version-policy/):
* defined command alias `check` which runs code quality checks, for
example: [scalafmt](https://scalameta.org/scalafmt/) and binary compatibility check
by [sbt-version-policy](https://github.com/scalacenter/sbt-version-policy/):
```sbt
addCommandAlias("fmt", "all scalafmtAll scalafmtSbt; scalafixEnable; scalafixAll") // optional: for development
addCommandAlias("check", "all versionPolicyCheck Compile/doc scalafmtCheckAll scalafmtSbtCheck; scalafixEnable; scalafixAll --check")
addCommandAlias("build", "all compile test") // optional: for development
addCommandAlias("fmt", "all scalafmtRepo") // optional: for development
addCommandAlias("check", "all versionPolicyCheck Compile/doc scalafmtCheckRepo")
addCommandAlias("build", "+all compile testFull") // optional: for development
```
as very minimum "no-op" placeholder:
```sbt
Expand Down