diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..812f02b --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,37 @@ +name-template: "$RESOLVED_VERSION" +tag-template: "$RESOLVED_VERSION" + +categories: + - title: Breaking Changes + labels: [breaking] + - title: Features + labels: [feat] + - title: Bug Fixes + labels: [fix] + - title: Refactoring + labels: [refactor] + - title: Documentation + labels: [docs] + - title: Testing + labels: [test] + - title: Build + labels: [build] + +version-resolver: + minor: + labels: [breaking] + default: patch + +template: | + $CHANGES +change-template: "- $TITLE (#$NUMBER)" +change-title-escapes: '\<*_&@' +no-changes-template: "No changes" +category-template: "### $TITLE" + +exclude-labels: + - release + +replacers: + - search: '/- (feat|fix|refactor|docs|ci|chore|test|build|release)(\(.+?\))?!?:\s*/g' + replace: "- " diff --git a/.github/workflows/coverage-badge.yml b/.github/workflows/coverage-badge.yml new file mode 100644 index 0000000..5997808 --- /dev/null +++ b/.github/workflows/coverage-badge.yml @@ -0,0 +1,17 @@ +name: Publish Coverage Badge + +on: + push: + branches: + - main + +jobs: + publish-coverage-badge: + runs-on: ubuntu-22.04 + steps: + - name: Trigger transformplan-static to update coverage badge + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.TRANSFORMPLAN_STATIC_TOKEN }} + repository: limebit/transformplan-static + event-type: publish-coverage-badge diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..cd35a09 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,31 @@ +name: Check Python Coverage + +on: + push: + branches: [main] + pull_request: + +jobs: + coverage: + runs-on: ubuntu-22.04 + permissions: + pull-requests: write + + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + python-version: "3.10" + + - name: Install dependencies + run: | + uv sync --group tests + + - name: Create coverage report + run: | + uv run coverage run -m pytest + uv run coverage report -m --fail-under=99 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3b93e0d..dfc8bd9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 @@ -40,7 +40,7 @@ jobs: deploy: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 4e91909..09f4373 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -5,7 +5,7 @@ on: jobs: format: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 @@ -18,7 +18,8 @@ jobs: python-version: "3.10" - name: Install dependencies - run: uv sync --group dev + run: | + uv sync --group dev - name: Format with ruff run: uv run ruff format --check diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f05e1bf..b1c617c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -5,7 +5,7 @@ on: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 @@ -18,7 +18,8 @@ jobs: python-version: "3.10" - name: Install dependencies - run: uv sync --group dev + run: | + uv sync --group dev - name: Lint with ruff run: uv run ruff check --output-format=github . diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index dc0f86a..b790706 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -6,7 +6,7 @@ on: jobs: comment: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: pull-requests: write @@ -25,7 +25,7 @@ jobs: * Documentation is updated if necessary. `; - github.rest.issues.createComment({ + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..278ee27 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,65 @@ +name: PR Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + validate-title: + runs-on: ubuntu-22.04 + + steps: + - name: Validate PR title + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + refactor + docs + ci + chore + test + build + release + requireScope: false + + label-pr: + needs: validate-title + runs-on: ubuntu-22.04 + + steps: + - name: Label PR by type + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + const prNumber = context.payload.pull_request.number; + + const typeMatch = title.match(/^(\w+)/); + const labelsToAdd = []; + const type = typeMatch ? typeMatch[1] : ''; + + const changelogTypes = ['feat', 'fix', 'refactor', 'docs', 'test', 'build']; + if (changelogTypes.includes(type)) { + labelsToAdd.push(type); + } + + if (type === 'release') { + labelsToAdd.push('release'); + } + + if (labelsToAdd.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: labelsToAdd, + }); + } diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..b516438 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,95 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + version: + description: "Version override (leave empty to use release-drafter's resolved version)" + required: false + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Get draft release + id: draft + uses: release-drafter/release-drafter@v6 + with: + disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine version + id: version + env: + VERSION_OVERRIDE: ${{ inputs.version }} + RESOLVED_VERSION: ${{ steps.draft.outputs.resolved_version }} + run: | + if [ -n "$VERSION_OVERRIDE" ]; then + VERSION="$VERSION_OVERRIDE" + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Version must be in semver format (e.g., 0.2.0)" + exit 1 + fi + else + VERSION="$RESOLVED_VERSION" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Update changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ steps.version.outputs.version }} + release-notes: ${{ steps.draft.outputs.body }} + path-to-changelog: CHANGELOG.md + heading-text: "[${{ steps.version.outputs.version }}]" + + - name: Bump version + env: + VERSION: ${{ steps.version.outputs.version }} + run: sed -i '0,/^version = ".*"/s//version = "'"$VERSION"'"/' pyproject.toml + + - name: Update uv.lock + run: uv lock + + - name: Stage files + run: git add CHANGELOG.md pyproject.toml uv.lock + + - name: Create release PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + BRANCH="release/${VERSION}" + + git checkout -b "$BRANCH" + git -c user.name="github-actions[bot]" \ + -c user.email="github-actions[bot]@users.noreply.github.com" \ + commit -m "release: ${VERSION}" + git push origin "$BRANCH" + + BODY="## Release ${VERSION} + + This PR was automatically generated by the release workflow. + + ### Checklist + - [ ] Changelog looks correct + - [ ] Version numbers are correct" + + gh pr create \ + --title "release: ${VERSION}" \ + --body "$BODY" \ + --base main \ + --head "$BRANCH" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9e92dc7..272e35f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,26 +1,126 @@ -name: Publish Package +name: Publish Release on: - workflow_dispatch: + push: + branches: [main] + +concurrency: + group: publish-release + cancel-in-progress: false + +permissions: + contents: write + actions: write jobs: - publish: - runs-on: ubuntu-latest + detect-release: + runs-on: ubuntu-22.04 + outputs: + is_release: ${{ steps.check.outputs.is_release }} + version: ${{ steps.check.outputs.version }} + tag: ${{ steps.check.outputs.tag }} steps: - - uses: actions/checkout@v5 + - name: Check for release commit + id: check + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) + + if echo "$FIRST_LINE" | grep -qE '^release: [0-9]+\.[0-9]+\.[0-9]+'; then + VERSION=$(echo "$FIRST_LINE" | sed 's/release: \([0-9.]*\).*/\1/') + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=${VERSION}" >> "$GITHUB_OUTPUT" + else + echo "is_release=false" >> "$GITHUB_OUTPUT" + fi + + build: + needs: detect-release + if: needs.detect-release.outputs.is_release == 'true' + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - cache-dependency-glob: pyproject.toml - python-version: "3.10" - name: Build package run: uv build + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + publish: + needs: [detect-release, build] + runs-on: ubuntu-22.04 + + steps: + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - name: Publish to PyPI - run: uv publish + run: uv publish dist/* env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} + + release: + needs: [detect-release, publish] + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Finalize GitHub release + id: release + uses: release-drafter/release-drafter@v6 + with: + name: ${{ needs.detect-release.outputs.version }} + tag: ${{ needs.detect-release.outputs.tag }} + version: ${{ needs.detect-release.outputs.version }} + commitish: ${{ github.sha }} + disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish GitHub release + run: gh release edit "$TAG" --draft=false + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.release.outputs.tag_name }} + + - name: Upload artifacts to release + run: gh release upload "$TAG" dist/* + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.release.outputs.tag_name }} + + - name: Trigger release workflows + uses: peter-evans/repository-dispatch@v2 + with: + event-type: python-release + client-payload: > + { + "version": "${{ needs.detect-release.outputs.version }}", + "tag": "${{ needs.detect-release.outputs.tag }}" + } diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..d07efb0 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,25 @@ +name: Release Drafter + +on: + push: + branches: [main] + +concurrency: + group: release-drafter + cancel-in-progress: true + +permissions: + contents: write + pull-requests: read + +jobs: + draft-release: + runs-on: ubuntu-22.04 + + steps: + - name: Draft release + uses: release-drafter/release-drafter@v6 + with: + disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml new file mode 100644 index 0000000..d71ad67 --- /dev/null +++ b/.github/workflows/semver-checks.yml @@ -0,0 +1,87 @@ +name: Semver Checks + +on: + pull_request: + types: [opened, edited, synchronize, reopened, labeled] + paths: + - "transformplan/**" + - "tests/**" + - "pyproject.toml" + - "uv.lock" + +permissions: + contents: read + pull-requests: write + +jobs: + python-semver-checks: + if: github.event.action != 'edited' && github.event.action != 'labeled' + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Run griffe check + id: griffe + run: | + set +e + uvx griffe check transformplan \ + --against ${{ github.event.pull_request.base.sha }} \ + --format github + EXIT_CODE=$? + set -e + if [ $EXIT_CODE -eq 0 ]; then + echo "result=pass" >> "$GITHUB_OUTPUT" + elif [ $EXIT_CODE -eq 1 ]; then + echo "result=breaking" >> "$GITHUB_OUTPUT" + else + echo "::warning::griffe check failed with exit code $EXIT_CODE (not a breaking change detection)" + echo "result=error" >> "$GITHUB_OUTPUT" + fi + + - name: Label breaking changes + if: steps.griffe.outputs.result == 'breaking' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['breaking'], + }); + + check-breaking-title: + if: always() + needs: [python-semver-checks] + runs-on: ubuntu-22.04 + + steps: + - name: Check breaking labels match title + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + const isBreaking = /^\w+(\(\w+\))?!:/.test(title); + + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + + const labelNames = labels.map(l => l.name); + const hasBreakingLabel = labelNames.includes('breaking'); + + if (hasBreakingLabel && !isBreaking) { + core.setFailed( + 'PR has breaking change labels but title is missing the ! indicator. ' + + 'Update the title, e.g.: feat!: description' + ); + } diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..e7ba53b --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + uv sync --group tests + + - name: Run tests + run: | + uv run pytest -W error diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index cd7a24c..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Tests - -on: - push: - branches: [main] - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - - steps: - - uses: actions/checkout@v5 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - cache-dependency-glob: pyproject.toml - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --group tests - - - name: Run tests with coverage - run: uv run pytest -vv -W error --cov=transformplan --cov-report=xml --cov-report=term - - coverage-badge: - needs: test - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: write - - steps: - - uses: actions/checkout@v5 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - cache-dependency-glob: pyproject.toml - python-version: "3.10" - - - name: Install dependencies - run: uv sync --group tests - - - name: Generate coverage report - run: uv run pytest --cov=transformplan --cov-report=xml --cov-report=term - - - name: Create coverage badge - uses: tj-actions/coverage-badge-py@v2 - with: - output: coverage.svg - - - name: Commit badge - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add coverage.svg - git diff --staged --quiet || git commit -m "chore: update coverage badge [skip ci]" - git push