Skip to content
87 changes: 87 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -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:
issues: 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}`);
92 changes: 73 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,88 @@ jobs:
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

- name: Add Helm Repos
run: |
helm repo add stable https://charts.helm.sh/stable
helm repo add nuclio https://nuclio.github.io/nuclio/charts
helm repo add v3io-stable https://v3io.github.io/helm-charts/stable
helm repo add minio https://charts.min.io/
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add strimzi https://strimzi.io/charts/
helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm

- name: Run chart-releaser
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

- name: Extract Chart Version from Chart.yaml
id: extract_version
- 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
else
echo "is_rc=false" >> $GITHUB_OUTPUT
fi

# - name: Add Helm Repos
# run: |
# helm repo add stable https://charts.helm.sh/stable
# helm repo add nuclio https://nuclio.github.io/nuclio/charts
# helm repo add v3io-stable https://v3io.github.io/helm-charts/stable
# helm repo add minio https://charts.min.io/
# helm repo add spark-operator https://kubeflow.github.io/spark-operator
# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# helm repo add strimzi https://strimzi.io/charts/
# helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm

# - name: Run chart-releaser
# uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f
# env:
# CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

- name: Find first RC tag for this version
if: steps.version_check.outputs.is_rc == 'false'
id: first_rc
run: |
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

- 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:
CR_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
Expand Down
64 changes: 64 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading