Skip to content
Merged
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
102 changes: 102 additions & 0 deletions .github/workflows/api-diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: API compatibility

# Fails a pull request that breaks the HTTP API for existing clients. The OpenAPI document is generated by the
# backend's annotation processor (backend/build/classes/java/main/META-INF/swagger/quelea.yml), so the base and the
# head are each built from their own checkout and compared as files.

on:
pull_request:

permissions:
contents: read

jobs:
api-diff:
name: OpenAPI breaking changes
runs-on: ubuntu-latest
env:
SPEC_PATH: backend/build/classes/java/main/META-INF/swagger/quelea.yml
ONELITEFEATHER_MAVEN_USERNAME: ${{ secrets.ONELITEFEATHER_MAVEN_USERNAME }}
ONELITEFEATHER_MAVEN_PASSWORD: ${{ secrets.ONELITEFEATHER_MAVEN_PASSWORD }}
steps:
- name: Checkout the PR base
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
persist-credentials: false

- name: Checkout the PR head
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
path: head
persist-credentials: false

- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

- name: Build the OpenAPI document of the base
id: base
working-directory: base
run: |
./gradlew :backend:compileJava
mkdir -p "${GITHUB_WORKSPACE}/api-diff"
if [ -f "${SPEC_PATH}" ]; then
cp "${SPEC_PATH}" "${GITHUB_WORKSPACE}/api-diff/base.yml"
echo "present=true" >> "${GITHUB_OUTPUT}"
else
echo "present=false" >> "${GITHUB_OUTPUT}"
fi

- name: Build the OpenAPI document of the head
if: steps.base.outputs.present == 'true'
working-directory: head
run: |
./gradlew :backend:compileJava
cp "${SPEC_PATH}" "${GITHUB_WORKSPACE}/api-diff/head.yml"

- name: Explain the skipped check
if: steps.base.outputs.present != 'true'
run: echo "::notice::The base has no quelea.yml, so there is no API to compare against. Skipping the check."

- name: Summarize the API changes
if: steps.base.outputs.present == 'true'
uses: oasdiff/oasdiff-action/changelog@v0.1.21
with:
base: api-diff/base.yml
revision: api-diff/head.yml
format: markdown
output-to-file: api-diff/changelog.md
# Keep the specs inside CI: no upload to oasdiff.com and no PR comment.
review: "false"
github-token: ""

- name: Write the changelog to the job summary
if: steps.base.outputs.present == 'true'
run: |
{
echo "## API changelog"
echo
if [ -s api-diff/changelog.md ]; then
cat api-diff/changelog.md
else
echo "No API changes."
fi
} >> "${GITHUB_STEP_SUMMARY}"

- name: Fail on breaking changes
if: steps.base.outputs.present == 'true'
uses: oasdiff/oasdiff-action/breaking@v0.1.21
with:
base: api-diff/base.yml
revision: api-diff/head.yml
fail-on: ERR
review: "false"
github-token: ""
47 changes: 47 additions & 0 deletions .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,50 @@ jobs:
java-distribution: "temurin"
run-tests: true
secrets: inherit

# The shared gradle-build-pr workflow has no coverage upload, so this job follows Ingot: a JaCoCo run of both
# test modules published to Codecov. It runs the suite a second time next to the shared build; the cost is
# accepted to keep the coverage independent of the reusable workflow. Codecov runs with fail_ci_if_error false
# and no threshold, so coverage never fails the build.
coverage:
name: Coverage
runs-on: ubuntu-latest
env:
ONELITEFEATHER_MAVEN_USERNAME: ${{ secrets.ONELITEFEATHER_MAVEN_USERNAME }}
ONELITEFEATHER_MAVEN_PASSWORD: ${{ secrets.ONELITEFEATHER_MAVEN_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

- name: Run tests with coverage
run: ./gradlew :backend:test :velocity:test jacocoTestReport

- name: Upload JaCoCo reports
if: always()
uses: actions/upload-artifact@v7
with:
name: jacoco-reports
path: |
backend/build/reports/jacoco/
velocity/build/reports/jacoco/
if-no-files-found: ignore
retention-days: 14

- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
files: backend/build/reports/jacoco/test/jacocoTestReport.xml,velocity/build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: false
# Tokenless uploads are rate limited. Set CODECOV_TOKEN in the repository secrets.
token: ${{ secrets.CODECOV_TOKEN }}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ Integration tests use Testcontainers and need a running Docker daemon. They are

Specifications and the change history live in `openspec/`.

## CI

Every pull request runs:

- **Build:** the shared OneLiteFeather Gradle workflow (`build test`, JDK 25).
- **API compatibility:** `oasdiff` compares the OpenAPI document of the base with the head and fails on breaking changes. The changelog goes to the job summary (`.github/workflows/api-diff.yml`).
- **Coverage:** JaCoCo XML and HTML reports for `backend` and `velocity`, uploaded to Codecov and as a workflow artifact. Coverage has no threshold and never fails the build (`.github/workflows/build-pr.yml`).

## License

[AGPL-3.0](LICENSE)
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ subprojects {
dependsOn(project.tasks.named("test"))
reports {
xml.required.set(true)
html.required.set(true)
}
}
getByName<Test>("test") {
useJUnitPlatform()
// Coverage reports follow every test run, so `./gradlew test` leaves them in build/reports/jacoco.
finalizedBy("jacocoTestReport")
testLogging {
events("passed", "skipped", "failed")
}
Expand Down
Loading