diff --git a/.github/scripts/normalize_coverage.py b/.github/scripts/normalize_coverage.py new file mode 100644 index 0000000..3478e03 --- /dev/null +++ b/.github/scripts/normalize_coverage.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import sqlite3 + +connection = sqlite3.connect(".coverage") + +# Normalize windows paths +connection.execute("UPDATE file SET path = REPLACE(path, '\\', '/')") + +connection.commit() +connection.close() diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..b7b37bd --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,37 @@ +--- +name: Dependabot auto-merge +on: pull_request_target + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Generate token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ steps.app-token.outputs.token }}" + + - name: Approve a PR + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9f160c3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,43 @@ +--- +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +# Cancel already running workflows if new ones are scheduled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validation: + uses: ./.github/workflows/validation.yml + + unit-tests: + uses: ./.github/workflows/unit-tests.yml + + # Produce a pull request payload artifact with various data about the + # pull-request event (such as the PR number, title, author, ...). + # This data is then be picked up by status-embed.yml action. + pr_artifact: + name: Produce Pull Request payload artifact + runs-on: ubuntu-latest + + steps: + - name: Prepare Pull Request Payload artifact + id: prepare-artifact + if: always() && github.event_name == 'pull_request' + continue-on-error: true + run: cat $GITHUB_EVENT_PATH | jq '.pull_request' > pull_request_payload.json + + - name: Upload a Build Artifact + if: always() && steps.prepare-artifact.outcome == 'success' + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: pull-request-payload + path: pull_request_payload.json diff --git a/.github/workflows/status_embed.yml b/.github/workflows/status_embed.yml new file mode 100644 index 0000000..fea615e --- /dev/null +++ b/.github/workflows/status_embed.yml @@ -0,0 +1,64 @@ +--- +name: Status Embed + +on: + workflow_run: + workflows: + - CI + types: + - completed + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + status_embed: + name: Send Status Embed to Discord + runs-on: ubuntu-latest + + steps: + # A workflow_run event does not contain all the information + # we need for a PR embed. That's why we upload an artifact + # with that information in the CI workflow. + - name: Get Pull Request Information + id: pr_info + if: github.event.workflow_run.event == 'pull_request' + run: | + curl -s -H "Authorization: token $GITHUB_TOKEN" ${{ github.event.workflow_run.artifacts_url }} > artifacts.json + DOWNLOAD_URL=$(cat artifacts.json | jq -r '.artifacts[] | select(.name == "pull-request-payload") | .archive_download_url') + [ -z "$DOWNLOAD_URL" ] && exit 1 + curl -sSL -H "Authorization: token $GITHUB_TOKEN" -o pull_request_payload.zip $DOWNLOAD_URL || exit 2 + unzip -p pull_request_payload.zip > pull_request_payload.json + [ -s pull_request_payload.json ] || exit 3 + echo "pr_author_login=$(jq -r '.user.login // empty' pull_request_payload.json)" >> $GITHUB_OUTPUT + echo "pr_number=$(jq -r '.number // empty' pull_request_payload.json)" >> $GITHUB_OUTPUT + echo "pr_title=$(jq -r '.title // empty' pull_request_payload.json)" >> $GITHUB_OUTPUT + echo "pr_source=$(jq -r '.head.label // empty' pull_request_payload.json)" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Send an informational status embed to Discord instead of the + # standard embeds that Discord sends. This embed will contain + # more information and we can fine tune when we actually want + # to send an embed. + - name: GitHub Actions Status Embed for Discord + uses: SebastiaanZ/github-status-embed-for-discord@v0.3.0 + with: + # Our GitHub Actions webhook + webhook_id: "1051784242318815242" + webhook_token: ${{ secrets.webhook_token }} + + # We need to provide the information of the workflow that + # triggered this workflow instead of this workflow. + workflow_name: ${{ github.event.workflow_run.name }} + run_id: ${{ github.event.workflow_run.id }} + run_number: ${{ github.event.workflow_run.run_number }} + status: ${{ github.event.workflow_run.conclusion }} + sha: ${{ github.event.workflow_run.head_sha }} + + # Now we can use the information extracted in the previous step: + pr_author_login: ${{ steps.pr_info.outputs.pr_author_login }} + pr_number: ${{ steps.pr_info.outputs.pr_number }} + pr_title: ${{ steps.pr_info.outputs.pr_title }} + pr_source: ${{ steps.pr_info.outputs.pr_source }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..882eb12 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,93 @@ +--- +name: Unit-Tests + +on: workflow_call + +jobs: + unit-tests: + runs-on: ${{ matrix.platform }} + + strategy: + fail-fast: false # Allows for matrix sub-jobs to fail without cancelling the rest + matrix: + platform: [ubuntu-latest, windows-latest] + python-version: ["3.8", "3.12"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup poetry + id: poetry_setup + uses: ItsDrike/setup-poetry@v1 + with: + python-version: ${{ matrix.python-version }} + install-args: "--without lint --without release" + + - name: Run pytest + shell: bash + run: | + poetry run task test + + python .github/scripts/normalize_coverage.py + mv .coverage .coverage.${{ matrix.platform }}.${{ matrix.python-version }} + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage.${{ matrix.platform }}.${{ matrix.python-version }} + path: .coverage.${{ matrix.platform }}.${{ matrix.python-version }} + retention-days: 1 + if-no-files-found: error + + upload-coverage: + needs: [unit-tests] + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup poetry + id: poetry_setup + uses: ItsDrike/setup-poetry@v1 + with: + python-version: 3.12 + install-args: "--no-root --only test" + + - name: Download all coverage artifacts + uses: actions/download-artifact@v4 + with: + pattern: coverage.* + merge-multiple: true # support downloading multiple artifacts to same dir + + # Combine all of the coverage files (for each os, python version - from matrix) + # into a single coverage file (.coverage), and produce a final (combined) coverage report. + - name: Combine coverage + run: | + coverage combine + coverage xml + coverage report + + - name: Upload coverage to codeclimate + uses: paambaati/codeclimate-action@v8.0.0 + env: + CC_TEST_REPORTER_ID: 0ec6191ea237656410b90dded9352a5b16d68f8d86d60ea8944abd41d532e869 + with: + coverageLocations: .coverage.xml:coverage.py + + tests-done: + needs: [unit-tests] + if: always() && !cancelled() + runs-on: ubuntu-latest + + steps: + - name: Set status based on required jobs + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + for result in $RESULTS; do + if [ "$result" != "success" ]; then + exit 1 + fi + done diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..c21359e --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,49 @@ +--- +name: Validation + +on: workflow_call + +env: + PRE_COMMIT_HOME: "/home/runner/.cache/pre-commit" + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup poetry + id: poetry_setup + uses: ItsDrike/setup-poetry@v1 + with: + python-version: 3.12 + install-args: "--without release" + + - name: Pre-commit Environment Caching + uses: actions/cache@v4 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: + "precommit-${{ runner.os }}-${{ steps.poetry_setup.outputs.python-version }}-\ + ${{ hashFiles('./.pre-commit-config.yaml') }}" + # Restore keys allows us to perform a cache restore even if the full cache key wasn't matched. + # That way we still end up saving new cache, but we can still make use of the cache from previous + # version. + restore-keys: "precommit-${{ runner.os }}-${{ steps.poetry_setup.outputs-python-version}}-" + + - name: Run pre-commit hooks + run: SKIP=ruff-linter,ruff-formatter,slotscheck,pyright pre-commit run --all-files + + - name: Run ruff linter + run: ruff check --output-format=github --show-fixes --exit-non-zero-on-fix . + + - name: Run ruff formatter + run: ruff format --diff . + + - name: Run slotscheck + run: slotscheck -m mcproto + + - name: Run pyright type checker + run: pyright .