diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..8cd8d59 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,54 @@ +name: Build & Test + +on: + workflow_call: + outputs: + artifact-name: + description: "Uploaded artifact name for downstream jobs" + value: ${{ jobs.build.outputs.artifact-name }} + +jobs: + build: + name: Build & Test + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + outputs: + artifact-name: ${{ steps.meta.outputs.artifact-name }} + steps: + - name: Checkout source + uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + cache: gradle + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run CI quality gate + run: ./gradlew clean check bootJar --stacktrace --no-daemon + + - name: Set artifact name + id: meta + run: echo "artifact-name=build-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" + + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.meta.outputs.artifact-name }} + path: | + build/libs/*.jar + build/reports/detekt/** + build/reports/ktlint/** + build/reports/jacoco/** + build/reports/** + build/test-results/** + build/jacoco/** + retention-days: 7 + if-no-files-found: error diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ece7eb1 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,86 @@ +name: CI +run-name: "${{ github.ref_name }} - ${{ github.event_name }} by @${{ github.actor }}" + +on: + push: + branches: [develop, main] + tags: ["v*"] + pull_request: + branches: [develop, main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + trufflehog: + name: TruffleHog Secret Scan + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout source + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Run TruffleHog + uses: trufflesecurity/trufflehog@main + with: + extra_args: --results=verified,unknown + + build: + name: Build & Test + needs: [trufflehog] + uses: ./.github/workflows/build.yaml + + docker: + name: Docker + needs: build + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') + uses: ./.github/workflows/docker.yaml + permissions: + contents: read + packages: write + with: + push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} + secrets: inherit + + notify-main: + name: Notify Main Build + needs: docker + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Send Telegram notification + env: + TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + DIGEST: ${{ needs.docker.outputs.image-digest }} + run: | + COMMIT_URL="https://github.com/${REPO}/commit/${SHA}" + MSG="*${REPO}* - main branch CI succeeded" + MSG="${MSG}%0A🔗 [Commit](${COMMIT_URL})" + if [ -n "${DIGEST}" ]; then + MSG="${MSG}%0AImage: \`ghcr.io/${REPO}@${DIGEST}\`" + fi + curl -sf -X POST \ + "https://api.telegram.org/bot${TOKEN}/sendMessage" \ + -d "chat_id=${CHAT_ID}" \ + -d "parse_mode=Markdown" \ + -d "text=${MSG}" + + release: + name: Release + needs: docker + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + uses: ./.github/workflows/release.yaml + with: + version: ${{ github.ref_name }} + image-digest: ${{ needs.docker.outputs.image-digest }} + secrets: inherit diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..0854d36 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,77 @@ +name: Docker Build & Push + +on: + workflow_call: + inputs: + push: + description: "Push image to GHCR" + type: boolean + required: true + outputs: + image-digest: + description: "Pushed image digest (sha256:…)" + value: ${{ jobs.docker.outputs.image-digest }} + image-tags: + description: "Comma-separated list of applied tags" + value: ${{ jobs.docker.outputs.image-tags }} + +jobs: + docker: + name: Docker Build & Push + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + image-digest: ${{ steps.build-push.outputs.digest }} + image-tags: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout source + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build image (no push) + if: inputs.push == false + uses: docker/build-push-action@v7 + with: + context: . + file: ./Containerfile + push: false + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Log in to GHCR + if: inputs.push == true + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + if: inputs.push == true + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=branch + type=ref,event=pr + type=sha,prefix=sha- + + - name: Build and push image + if: inputs.push == true + id: build-push + uses: docker/build-push-action@v7 + with: + context: . + file: ./Containerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..0fdfcb1 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,58 @@ +name: Release & Notify + +on: + workflow_call: + inputs: + version: + description: "Tag name, e.g. v1.2.3" + type: string + required: true + image-digest: + description: "Docker image digest from docker job" + type: string + required: false + default: "" + secrets: + TELEGRAM_BOT_TOKEN: + required: true + TELEGRAM_CHAT_ID: + required: true + +jobs: + release: + name: GitHub Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + if gh release view "${{ inputs.version }}" >/dev/null 2>&1; then + echo "Release ${{ inputs.version }} already exists. Skipping creation." + else + gh release create "${{ inputs.version }}" \ + --generate-notes \ + --title "${{ inputs.version }}" + fi + + - name: Send Telegram notification + env: + TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + VERSION: ${{ inputs.version }} + REPO: ${{ github.repository }} + DIGEST: ${{ inputs.image-digest }} + run: | + RELEASE_URL="https://github.com/${REPO}/releases/tag/${VERSION}" + MSG="*${REPO}* - released *${VERSION}*" + MSG="${MSG}%0A🔗 [Release notes](${RELEASE_URL})" + if [ -n "${DIGEST}" ]; then + MSG="${MSG}%0AImage: \`ghcr.io/${REPO}@${DIGEST}\`" + fi + curl -sf -X POST \ + "https://api.telegram.org/bot${TOKEN}/sendMessage" \ + -d "chat_id=${CHAT_ID}" \ + -d "parse_mode=Markdown" \ + -d "text=${MSG}" diff --git a/.gitignore b/.gitignore index ee59915..795dcc9 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ gradle-app.setting *.tar.gz *.rar +# Gradle wrapper +!gradle/wrapper/gradle-wrapper.jar + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..61285a6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ