Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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` +
`Example: [Feat] Add SeaweedFS bucket auto-creation\n` +
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 example [Feat] Add SeaweedFS bucket auto-creation would fail this very validation. Feat lowercased is feat, which is not in allowedScopes (the list has feature, not feat). Either change the example to [Feature] ... or change the scope name to feat.

`Allowed scopes: ${allowedScopes.join(', ')}`
);
return;
}

const scope = match[1].toLowerCase();

if (!allowedScopes.includes(scope)) {
core.setFailed(
`Invalid scope "[${match[1]}]".\n` +
`Allowed scopes: ${allowedScopes.join(', ')}\n` +
`Example: [Feat] Add SeaweedFS bucket auto-creation`
);
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}`);
54 changes: 45 additions & 9 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,39 @@ 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
fi
echo "version=$CHART_VERSION" >> $GITHUB_OUTPUT
VERSION="${{ steps.version_check.outputs.version }}"
FIRST_RC=$(git tag --sort=version:refname \
| grep "^mlrun-ce-${VERSION}-rc." \
| head -1)
echo "tag=${FIRST_RC}" >> $GITHUB_OUTPUT
echo "First RC tag: ${FIRST_RC}"
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.

Not sure if can happen, but If no RC tag exists for this version (e.g., a hotfix release that skipped RC), FIRST_RC will be empty. The downstream git-cliff step would then receive ^..HEAD as the range, which is invalid and will fail the workflow. you can add a guard if you think necessary.


- 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.tag }}^..HEAD
--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 }}" \
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
67 changes: 67 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[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 going forward): [scope] ...
{ message = "(?i)^\\[feat\\]", group = "Features" },
{ message = "(?i)^\\[fix\\]", group = "Bug Fixes" },
{ message = "(?i)^\\[perf\\]", group = "Performance" },
{ message = "(?i)^\\[refactor\\]", group = "Refactor" },
{ message = "(?i)^\\[docs?\\]", group = "Documentation" },
{ message = "(?i)^\\[chore\\]", group = "Miscellaneous" },
{ message = "(?i)^\\[revert\\]", group = "Reverts" },
{ message = "(?i)^\\[breaking\\]", group = "Breaking Changes" },

# conventional commits format: feat: / fix: ...
{ message = "(?i)^feat", group = "Features" },
{ message = "(?i)^fix", group = "Bug Fixes" },
{ message = "(?i)^perf", group = "Performance" },
{ message = "(?i)^refactor", group = "Refactor" },
{ message = "(?i)^docs?", group = "Documentation" },
{ message = "(?i)^chore\\(deps\\)", group = "Dependencies" },
{ message = "(?i)^chore", group = "Miscellaneous" },
{ 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)^\\[[^\\]]+\\].*(disable|remove|clean|deprecat)", group = "Miscellaneous" },
{ message = "(?i)^\\[[^\\]]+\\]", group = "Changes" },

# 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 = "Miscellaneous" },

# 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