diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 00000000..28673fa7 --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,87 @@ +name: PR Validation + +on: + pull_request_target: + types: [opened, edited, reopened] +concurrency: + group: pr-validation-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate-and-label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Validate PR title and assign label + uses: actions/github-script@v7 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + script: | + const title = context.payload.pull_request.title; + const prNumber = context.payload.pull_request.number; + + const allowedScopes = ['feature', 'fix', 'docs', 'improvement', 'revert', 'breaking', 'ci']; + + const scopeToLabel = { + feature: 'feature', + ci: 'ci', + fix: 'bug', + docs: 'documentation', + improvement: 'improvement', + revert: 'revert', + breaking: 'breaking-change', + }; + + const match = title.match(/^\[([^\]]+)\]\s+\S+/); + + if (!match) { + core.setFailed( + `PR title must follow the format: [scope] description\n` + + `Allowed scopes (case-insensitive): [Feature], [Fix], [Docs], [Improvement], [Revert], [Breaking], [CI]\n` + + `Example: [Feature] Add SeaweedFS bucket auto-creation` + ); + return; + } + + const scope = match[1].toLowerCase(); + + if (!allowedScopes.includes(scope)) { + core.setFailed( + `Invalid scope "[${match[1]}]".\n` + + `Allowed scopes (case-insensitive): [Feature], [Fix], [Docs], [Improvement], [Revert], [Breaking], [CI]\n` + + `Example: [Fix] Resolve crash on startup` + ); + return; + } + + const labelName = scopeToLabel[scope]; + + // Remove any stale scope labels from a previous title edit + const allScopeLabels = Object.values(scopeToLabel); + const currentLabels = context.payload.pull_request.labels.map(l => l.name); + + for (const stale of currentLabels) { + if (allScopeLabels.includes(stale) && stale !== labelName) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: stale, + }); + core.info(`Removed stale label: ${stale}`); + } + } + + // Apply the correct label if not already present + if (!currentLabels.includes(labelName)) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [labelName], + }); + } + + core.info(`PR title valid — scope: [${scope}] → label: ${labelName}`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2cfb4f2..2b062cd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,23 @@ jobs: git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - name: Extract Chart Version and check RC + id: version_check + run: | + CHART_VERSION=$(grep '^version:' charts/mlrun-ce/Chart.yaml | awk '{print $2}') + if [[ -z "$CHART_VERSION" ]]; then + echo "Error: Failed to extract version from Chart.yaml" >&2 + exit 1 + fi + echo "version=$CHART_VERSION" >> $GITHUB_OUTPUT + if [[ "$CHART_VERSION" =~ -rc ]]; then + echo "is_rc=true" >> $GITHUB_OUTPUT + echo "Chart version: $CHART_VERSION (RC release)" + else + echo "is_rc=false" >> $GITHUB_OUTPUT + echo "Chart version: $CHART_VERSION (stable release)" + fi + - name: Add Helm Repos run: | helm repo add stable https://charts.helm.sh/stable @@ -52,18 +69,57 @@ jobs: env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - name: Extract Chart Version from Chart.yaml - id: extract_version + - name: Find first RC tag for this version + if: steps.version_check.outputs.is_rc == 'false' + id: first_rc run: | - CHART_VERSION=$(grep '^version:' charts/mlrun-ce/Chart.yaml | awk '{print $2}') - if [[ -z "$CHART_VERSION" ]]; then - echo "Error: Failed to extract version from Chart.yaml" >&2 - exit 1 + VERSION="${{ steps.version_check.outputs.version }}" + + FIRST_RC=$(git tag --sort=version:refname \ + | grep "^mlrun-ce-${VERSION}-rc\." \ + | head -1) + + if [[ -n "$FIRST_RC" ]]; then + echo "range=${FIRST_RC}^..HEAD" >> $GITHUB_OUTPUT + echo "Range start: first RC tag ${FIRST_RC}" + else + # Hotfix with no RC — use previous stable tag so notes cover only this version + PREV_STABLE=$(git tag --sort=version:refname \ + | grep "^mlrun-ce-[0-9]" \ + | grep -v "\-rc\." \ + | grep -v "^mlrun-ce-${VERSION}$" \ + | tail -1) + if [[ -n "$PREV_STABLE" ]]; then + echo "range=${PREV_STABLE}..HEAD" >> $GITHUB_OUTPUT + echo "Range start: previous stable tag ${PREV_STABLE}" + else + echo "range=HEAD" >> $GITHUB_OUTPUT + echo "Range start: none (first ever release)" + fi fi - echo "version=$CHART_VERSION" >> $GITHUB_OUTPUT + + - name: Generate release notes with git-cliff + if: steps.version_check.outputs.is_rc == 'false' + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: >- + ${{ steps.first_rc.outputs.range }} + --tag mlrun-ce-${{ steps.version_check.outputs.version }} + --ignore-tags "mlrun-ce-${{ steps.version_check.outputs.version }}-rc.*" + env: + OUTPUT: RELEASE_NOTES.md + + - name: Update GitHub Release with release notes + if: steps.version_check.outputs.is_rc == 'false' + env: + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + gh release edit "mlrun-ce-${{ steps.version_check.outputs.version }}" \ + --notes-file RELEASE_NOTES.md outputs: - version: ${{ steps.extract_version.outputs.version }} + version: ${{ steps.version_check.outputs.version }} deploy_ce_onprem: needs: release diff --git a/charts/mlrun-ce/Chart.yaml b/charts/mlrun-ce/Chart.yaml index 7f066992..16ed4151 100644 --- a/charts/mlrun-ce/Chart.yaml +++ b/charts/mlrun-ce/Chart.yaml @@ -8,4 +8,4 @@ sources: [] maintainers: - name: MLRun email: mlrun@iguazio.com - url: https://www.mlrun.org/ + url: https://www.mlrun.org/ \ No newline at end of file diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..aa741b03 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,64 @@ +[changelog] +header = "" +body = """ +{% for group, commits in commits | sort(attribute="group") | group_by(attribute="group") %}\ + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %}\ + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | split(pat="\n") | first | upper_first }} \ + ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/mlrun/ce/commit/{{ commit.id }}))\ + \n \ + {% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = false +filter_unconventional = false +split_commits = false +commit_preprocessors = [] +commit_parsers = [ + # skip merge commits + { message = "(?i)^merge", skip = true }, + + # new PR title format (enforced by pr-validation.yml): [scope] description + # allowed scopes: feature, fix, docs, improvement, revert, breaking, ci + { message = "(?i)^\\[feature\\]", group = "Features" }, + { message = "(?i)^\\[fix\\]", group = "Bug Fixes" }, + { message = "(?i)^\\[docs\\]", group = "Documentation" }, + { message = "(?i)^\\[improvement\\]", group = "Improvements" }, + { message = "(?i)^\\[revert\\]", group = "Reverts" }, + { message = "(?i)^\\[breaking\\]", group = "Breaking Changes" }, + { message = "(?i)^\\[ci\\]", group = "CI/CD" }, + + # conventional commits format: feat: / fix: ... + { message = "(?i)^feat", group = "Features" }, + { message = "(?i)^fix", group = "Bug Fixes" }, + { message = "(?i)^refactor", group = "Improvements" }, + { message = "(?i)^docs?", group = "Documentation" }, + { message = "(?i)^chore\\(deps\\)", group = "Improvements" }, + { message = "(?i)^revert", group = "Reverts" }, + + # historical [ComponentName] format — infer type from the verb in the message + { message = "(?i)^\\[[^\\]]+\\].*(fix|bug|broken|regression)", group = "Bug Fixes" }, + { message = "(?i)^\\[[^\\]]+\\].*(add|support|enable|upgrade|update|migrate|connect|expose|allow)", group = "Features" }, + { message = "(?i)^\\[[^\\]]+\\]", group = "Other" }, + + # plain English fallback + { message = "(?i)^(fix|bug)", group = "Bug Fixes" }, + { message = "(?i)^(add|update|upgrade|support|enable|migrate|expose|allow)", group = "Features" }, + { message = "(?i)^(remove|disable|clean|deprecat)", group = "Other" }, + + # catch-all — anything that didn't match above + { message = ".*", group = "Other" }, +] +protect_breaking_commits = false +filter_commits = false +tag_pattern = "mlrun-ce-[0-9].*" +skip_tags = "" +ignore_tags = "" +topo_order = false +sort_commits = "newest"