Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: PR Validation

on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
types: [opened, edited, reopened, synchronize]
types: [opened, edited, reopened]

you can't change title on push - not sure why it is needed. dropped synchronize


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add conmcurrency to avoid race conditions

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'];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowed scopes (feature, fix, docs, improvement, revert, breaking, ci) don't align with cliff.toml's commit parsers (feat, fix, perf, refactor, docs, chore, revert, breaking).

feature passes validation but won't match [feat] in cliff
improvement and ci pass validation but have no cliff parser at all
perf, refactor, chore are recognized by cliff but would fail validation
From what I understand, these two systems need to share the same vocabulary or the release notes categorization will be broken for newly enforced titles.


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}`);
70 changes: 62 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ 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
else
echo "is_rc=false" >> $GITHUB_OUTPUT
fi

- name: Add Helm Repos
run: |
helm repo add stable https://charts.helm.sh/stable
Expand All @@ -52,18 +67,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:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just use the same ENV the job has been using already - link if i change this should I change the old one also?

run: |
gh release edit "mlrun-ce-${{ steps.version_check.outputs.version }}" \
Comment thread
yaelgen marked this conversation as resolved.
--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