diff --git a/.github/helpers/gh_changelog_generator/gh_changelog_generator.py b/.github/helpers/gh_changelog_generator/gh_changelog_generator.py index 6ebfa4a07eb..2c7eae79360 100644 --- a/.github/helpers/gh_changelog_generator/gh_changelog_generator.py +++ b/.github/helpers/gh_changelog_generator/gh_changelog_generator.py @@ -18,20 +18,15 @@ JIRA_PROJECT = os.getenv("JIRA_PROJECT", "DEV").strip('\"') JIRA_RN_FIELD = os.getenv("JIRA_RN_FIELD", "customfield_10064").strip('\"') -AHA_SERVER = os.getenv("AHA_SERVER", "https://labelstudio.aha.io").strip('\"') -AHA_TOKEN = os.getenv("AHA_TOKEN").strip('\"') -AHA_PRODUCT = os.getenv("AHA_PRODUCT", "LSDV").strip('\"') -AHA_RN_FIELD = os.getenv("AHA_RN_FIELD", "release_notes").strip('\"') -AHA_FETCH_STRATEGY = os.getenv("AHA_FETCH_STRATEGY", "PARKING_LOT").strip('\"') # PARKING_LOT or TAG -AHA_TAG = os.getenv("AHA_TAG", "").strip('\"') -AHA_ADDITIONAL_RELEASES_TAG = os.getenv("AHA_ADDITIONAL_RELEASES_TAG", "").strip('\"') - GH_REPO = os.getenv("GH_REPO", "").strip('\"') GH_TOKEN = os.getenv("GH_TOKEN").strip('\"') # https://github.com/settings/tokens/new LAUNCHDARKLY_SDK_KEY = os.getenv("LAUNCHDARKLY_SDK_KEY", '').strip('\"') LAUNCHDARKLY_ENVIRONMENT = os.getenv("LAUNCHDARKLY_ENVIRONMENT", '').strip('\"') +HELM_CHART_REPO = os.getenv("HELM_CHART_REPO", None) +HELM_CHART_PATH = os.getenv("HELM_CHART_PATH", None) + OUTPUT_FILE_MD = os.getenv("OUTPUT_FILE_MD", 'output.md') OUTPUT_FILE_JSON = os.getenv("OUTPUT_FILE_JSON", 'output.json') @@ -57,117 +52,39 @@ DEFAULT_LABEL, ] - -class AHA: - def __init__(self, server: str, token: str): - self.server = server - self.token = token - - def query(self, url: str, data: dict = None, params: dict = None, method: str = 'GET'): - response = requests.request( - method=method, - url=f"{self.server}/{url}", - headers={ - "Authorization": f"Bearer {self.token}", - "Content-Type": "application/json", - "Accept": "application/json", - }, - json=data, - params=params, - ) - return response.json() - - def paginate(self, url: str, key: str, data: dict = None, method: str = 'GET', page: int = 0, per_page: int = 100): - result = [] - current_page = page - total_pages = None - while total_pages is None or current_page <= total_pages: - response_json = self.query( - url=url, - data=data, - params={"page": current_page + 1, "per_page": per_page}, - method=method, - ) - pagination = response_json.get('pagination', []) - current_page = int(pagination.get('current_page')) - total_pages = int(pagination.get('total_pages')) - entries = response_json.get(key, []) - result.extend(entries) - return result - - github_client = Github(GH_TOKEN) github_repo = github_client.get_repo(GH_REPO) jira_client = JIRA(JIRA_SERVER, basic_auth=(JIRA_USERNAME, JIRA_TOKEN)) -aha_client = AHA(AHA_SERVER, AHA_TOKEN) +FEATURE_FLAGS = {} -class AhaFeature: - pr = None - - def __init__(self, feature_num: str, pr: int = None): - self.type = "Aha! Feature" - self.pr = pr - feature = aha_client.query(f'api/v1/features/{feature_num}').get('feature') - self.key = str(feature.get('reference_num')) - self.status = str(feature.get('workflow_status').get('name')) - self.label = feature.get('workflow_kind', {}).get('name', DEFAULT_LABEL) - self.summary = str(feature.get('name')) - self.release_note = next( - (f.get('value') for f in feature.get('custom_fields', []) if f.get('key') == AHA_RN_FIELD), None) - self.desc = self.release_note if self.release_note else self.summary - self.link = str(feature.get('url')) - self.releases_tags = next( - (f.get('value') for f in feature.get('custom_fields', []) if f.get('key') == 'releases'), []) - - def set_releases_tags(self, tags: list[str]): - aha_client.query( - url=f'api/v1/features/{self.key}', - data={"feature": {"custom_fields": {"releases": tags}}}, - method='PUT', - ) - def __str__(self): - return f"<{self.link}|[{self.key}]>: {self.desc} -- *{self.status}*" +def ff_is_on(ff: dict) -> bool: + return ff.get('fallthrough').get('variation') == 0 if ff.get('on') else ff.get('offVariation') == 0 - def __key(self): - return self.key - def __dict__(self): - return { - "desc": self.desc, - "link": self.link, - "key": self.key, - "status": self.status, - "pr": self.pr - } +def ff_status(ff: dict) -> str: + is_on = ff_is_on(ff) + is_on_text = "On" if is_on else "Off" + options = [] + if ff.get('rules'): + options.append('rules') + if ff.get('targets'): + options.append('targets') + if options: + options_text = ' and '.join(options) + return f'{is_on_text} with {options_text}' + return is_on_text -class AhaRequirement(AhaFeature): - def __init__(self, feature_num: str, pr: int = None): - self.type = "Aha! Requirement" - self.pr = pr - feature = aha_client.query(f'api/v1/requirements/{feature_num}').get('requirement') - self.key = str(feature.get('reference_num')) - self.status = str(feature.get('workflow_status').get('name')) - self.label = feature.get('workflow_kind', {}).get('name', DEFAULT_LABEL) - self.summary = str(feature.get('name')) - self.release_note = next( - (f.get('value') for f in feature.get('custom_fields', []) if f.get('key') == AHA_RN_FIELD), None) - self.desc = self.release_note if self.release_note else self.summary - self.link = str(feature.get('url')) - self.releases_tags = next( - (f.get('value') for f in feature.get('custom_fields', []) if f.get('key') == 'releases'), []) +def ff_link(ff: dict) -> str: + key = ff.get('key') + return f'https://app.launchdarkly.com/default/{LAUNCHDARKLY_ENVIRONMENT}/features/{key}/targeting' - def set_releases_tags(self, tags: list[str]): - aha_client.query( - url=f'api/v1/requirements/{self.key}', - data={"feature": {"custom_fields": {"releases": tags}}}, - method='PUT', - ) +class JiraIssue: + pr = None -class JiraIssue(AhaFeature): def __init__(self, issue_number: str, pr: int = None): self.type = "Jira Issue" self.pr = pr @@ -180,15 +97,51 @@ def __init__(self, issue_number: str, pr: int = None): self.desc = self.release_note if self.release_note else self.summary self.link = f"{JIRA_SERVER}/browse/{self.key}" self.releases_tags = [] + self.ffs = self.get_ffs() def set_releases_tags(self, tags: list[str]): pass + def __str__(self): + return f"<{self.link}|[{self.key}]>: {self.desc} -- *{self.status}*" + + def __key(self): + return self.key + + def __dict__(self): + return { + "desc": self.desc, + "link": self.link, + "key": self.key, + "status": self.status, + "pr": self.pr, + "ffs": self.ffs, + } + + def get_ffs(self): + ff_key = self.key.lower().replace('-', '_') + result = [] + for name, ff in FEATURE_FLAGS.items(): + if ff_key in name: + key = ff.get('key') + on = ff_is_on(ff) + link = ff_link(ff) + status = ff_status(ff) + result.append( + { + 'key': key, + 'on': on, + 'link': link, + 'status': status, + } + ) + return result + TASK_CACHE = {} -def get_task(task_number: str, pr: int = None) -> AhaFeature or None: +def get_task(task_number: str, pr: int = None) -> JiraIssue or None: if task_number in TASK_CACHE.keys(): return TASK_CACHE.get(task_number) try: @@ -196,46 +149,10 @@ def get_task(task_number: str, pr: int = None) -> AhaFeature or None: TASK_CACHE[task_number] = task return task except Exception as e: - print(f'Could not find Issue {task_number} in Jira: {e}') - try: - task = AhaFeature(task_number, pr) - TASK_CACHE[task_number] = task - return task - except Exception as e: - print(f'Could not find Feature {task_number} in Aha!: {e}') - try: - task = AhaRequirement(task_number, pr) - TASK_CACHE[task_number] = task - return task - except Exception as e: - print(f'Could not find Requirement {task_number} in Aha!: {e}') + print(f'Could not find Issue {task_number} in Jira: {e}') return None -def get_aha_release(product: str, version: str): - aha_releases = aha_client.query(f'api/v1/products/{product}/releases').get('releases', []) - aha_sorted_releases = sorted(aha_releases, key=lambda x: x.get('name'), reverse=True) - return next((e for e in aha_sorted_releases if version in e.get('name')), None) - - -def get_aha_release_features(release_num: str) -> list[AhaFeature]: - features = aha_client.query(f'api/v1/releases/{release_num}/features').get('features', []) - tasks = set() - for feature in features: - if task := get_task(feature.get('reference_num')): - tasks.add(task) - return list(tasks) - - -def get_aha_release_features_by_tag(tag: str) -> list[AhaFeature]: - features = aha_client.paginate('api/v1/features', 'features', data={"tag": tag}) - tasks = set() - for feature in features: - if task := get_task(feature.get('reference_num')): - tasks.add(task) - return list(tasks) - - def get_jira_release(project: str, version: str): jira_project_versions = jira_client.project_versions(project=project) jira_sorted_project_versions = sorted(jira_project_versions, key=lambda x: x.name, reverse=True) @@ -256,7 +173,7 @@ def get_github_release(previous_ref: str, current_ref: str): return github_repo.compare(previous_ref, current_ref) -def get_github_release_tasks(commits) -> list[AhaFeature]: +def get_github_release_tasks(commits) -> list[JiraIssue]: tasks = set() for commit in commits: message_first_line = commit.commit.message.split("\n")[0] @@ -268,16 +185,13 @@ def get_github_release_tasks(commits) -> list[AhaFeature]: try: pr = int(match.group(5)) except Exception as e: - print(f'Could no parse pr from "{message_first_line}": {str(e)}') + print(f'Could not parse pr from "{message_first_line}": {str(e)}') if task := get_task(task_key, pr): tasks.add(task) - if AHA_ADDITIONAL_RELEASES_TAG: - task.set_releases_tags(list(set(task.releases_tags + [AHA_ADDITIONAL_RELEASES_TAG]))) return list(tasks) -def get_feature_flags() -> list[str]: - result = [] +def get_feature_flags() -> dict: if LAUNCHDARKLY_SDK_KEY: response = requests.get( url="https://sdk.launchdarkly.com/sdk/latest-all", @@ -286,35 +200,42 @@ def get_feature_flags() -> list[str]: }, timeout=30, ) - for key, flag in response.json().get('flags', {}).items(): - if not flag.get('on'): - result.append( - f'- [{key}]' - f'(https://app.launchdarkly.com/default/{LAUNCHDARKLY_ENVIRONMENT}/features/{key}/targeting)') - return result + return response.json().get('flags', {}) + return {} -def missing_tasks(left: list[AhaFeature], right: list[AhaFeature]) -> list[AhaFeature]: +def missing_tasks(left: list[JiraIssue], right: list[JiraIssue]) -> list[JiraIssue]: r_keys = [x.key for x in right] missing = [task for task in left if task.key not in r_keys] missing_sorted = sorted(missing, key=lambda x: int(x.key.split('-')[-1])) return missing_sorted -def sort_task_by_label(tasks: list[AhaFeature]) -> dict[str, list[AhaFeature]]: +def sort_task_by_label(tasks: list[JiraIssue]) -> dict[str, list[JiraIssue]]: result = {} for task in tasks: result[task.label] = result.get(task.label, []) + [task] return result -def render_tasks_md(tasks: list[AhaFeature]) -> list[str]: +def render_link_md(text: str, link: str) -> str: + return f"[{text}]({link})" + + +def render_tasks_md(tasks: list[JiraIssue]) -> list[str]: result = [] for task in tasks: - line = f'- {task.desc} [{task.key}]({task.link})' - if task.pr: - line += f' (#{task.pr})' - result.append(line) + summary = task.desc.replace('\n', ' ') + result.append(f'- {summary} {render_link_md(task.key, task.link)}') + return result + + +def render_ffs_md(ffs: list) -> list[str]: + result = [] + for ff in ffs: + key = ff.get('key') + link = ff_link(ff) + result.append(f'- {render_link_md(key, link)}') return result @@ -340,15 +261,18 @@ def render_add_header_md(title: str, lines: list[str]) -> list[str]: def render_output_md( gh_release, jira_release, - aha_release, - sorted_release_tasks: dict[str, list[AhaFeature]], - missing_in_gh: list[AhaFeature], - missing_in_tracker: list[AhaFeature], - missing_release_note_field: list[AhaFeature], - turned_off_feature_flags: list[str], + sorted_release_tasks: dict[str, list[JiraIssue]], + missing_in_gh: list[JiraIssue], + missing_in_tracker: list[JiraIssue], + missing_release_note_field: list[JiraIssue], + turned_off_feature_flags: list, + helm_chart_version: str = None, ) -> str: release_notes_lines = [] + if helm_chart_version: + release_notes_lines.append(f'Helm Chart version: {helm_chart_version}') + for label, tasks in sorted(sorted_release_tasks.items(), key=lambda x: LABEL_SORT.index(x[0]) if x[0] in LABEL_SORT else 100): release_notes_lines.extend( @@ -360,7 +284,7 @@ def render_output_md( comment = [] - comment.append(f'Full Changelog: [{PREVIOUS_REF}...{RELEASE_VERSION}]({gh_release.diff_url})') + comment.append(f'Full Changelog: [{PREVIOUS_REF}...{RELEASE_VERSION}]({gh_release.html_url})') comment.append( f'This changelog was updated in response to a push of {CURRENT_REF} [Workflow run]({WORKFLOW_RUN_LINK})') comment.append('') @@ -370,10 +294,6 @@ def render_output_md( f'{jira_release.id}/tab/release-report-all-issues)') else: comment.append('Jira Release not found') - if aha_release: - comment.append(f'[Aha! Release {RELEASE_VERSION}]({aha_release.get("url", "")})') - else: - comment.append('Aha! Release not found') if len(missing_in_tracker) == 0: comment.append('Release Notes are generated based on git log: No tasks found in Task Tracker.') @@ -405,7 +325,7 @@ def render_output_md( comment.extend( render_add_spoiler_md( f'Turned off Feature Flags ({len(turned_off_feature_flags)})', - turned_off_feature_flags + render_ffs_md(turned_off_feature_flags) ) ) @@ -420,7 +340,7 @@ def render_output_md( def render_output_json( - sorted_release_tasks: dict[str, list[AhaFeature]], + sorted_release_tasks: dict[str, list[JiraIssue]], ) -> dict: sorted_release_tasks_json = {} for label, tasks in sorted_release_tasks.items(): @@ -431,30 +351,29 @@ def render_output_json( return result +def get_helm_chart_version(repo: str, path: str) -> str or None: + chart_repo = github_client.get_repo(repo) + content = chart_repo.get_contents(path) + version_regexp = re.compile(r'version:\s*(.*)') + match = re.search(version_regexp, content.decoded_content.decode('utf-8')) + return match.group(1) + + def main(): gh_release = get_github_release(PREVIOUS_REF, CURRENT_REF) - print(f"{gh_release.html_url}") + print(f"Compare url: {gh_release.html_url}") print(f"Ahead by {gh_release.ahead_by}") print(f"Behind by {gh_release.behind_by}") print(f"Merge base commit: {gh_release.merge_base_commit}") print(f"Commits: {gh_release.commits}") - gh_release_tasks = get_github_release_tasks(gh_release.commits) - aha_release = None - aha_release_features = [] - if AHA_FETCH_STRATEGY == 'PARKING_LOT': - aha_release = get_aha_release(AHA_PRODUCT, RELEASE_VERSION) - if aha_release: - aha_release_features = get_aha_release_features(aha_release.get("reference_num", None)) - print(f"Aha! Release {aha_release.get('url', '')}") - else: - print("Aha! Release not found") - else: - if AHA_TAG: - aha_release = {'url': f'{AHA_SERVER}/api/v1/features?tag={AHA_TAG.replace(" ", "%20")}'} - aha_release_features = get_aha_release_features_by_tag(AHA_TAG) - else: - print("AHA TAG is not specified") + global FEATURE_FLAGS + try: + FEATURE_FLAGS = get_feature_flags() + except Exception as e: + print(f'Failed to fetch Feature Flags: {e}') + + gh_release_tasks = get_github_release_tasks(gh_release.commits) jira_release = get_jira_release(JIRA_PROJECT, RELEASE_VERSION) jira_release_issues = [] @@ -466,7 +385,7 @@ def main(): else: print("Jira Release not found") - tracker_release_tasks = jira_release_issues + aha_release_features + tracker_release_tasks = jira_release_issues if tracker_release_tasks: print(f"{len(tracker_release_tasks)} tasks found in Task Tracker") @@ -482,21 +401,25 @@ def main(): missing_in_gh = [] missing_in_tracker = [] missing_release_note_field = [x for x in tracker_release_tasks if not x.release_note] - turned_off_feature_flags = [] - try: - turned_off_feature_flags = get_feature_flags() - except Exception as e: - print(f'Failed to fetch Feature Flags: {e}') + + turned_off_feature_flags = [ff for name, ff in FEATURE_FLAGS.items() if not ff_is_on(ff)] + + helm_chart_version = None + if HELM_CHART_REPO and HELM_CHART_PATH: + try: + helm_chart_version = get_helm_chart_version(HELM_CHART_REPO, HELM_CHART_PATH) + except Exception as e: + print(f'Failed to fetch Helm Chart Version: {e}') output_md = render_output_md( gh_release, jira_release, - aha_release, sorted_release_tasks, missing_in_gh, missing_in_tracker, missing_release_note_field, turned_off_feature_flags, + helm_chart_version=helm_chart_version, ) if OUTPUT_FILE_MD: with open(OUTPUT_FILE_MD, 'w') as f: diff --git a/.github/workflows/cicd_pipeline.yml b/.github/workflows/cicd_pipeline.yml index 90e79eea5ab..40b2fca3f30 100644 --- a/.github/workflows/cicd_pipeline.yml +++ b/.github/workflows/cicd_pipeline.yml @@ -335,13 +335,6 @@ jobs: CURRENT_REF: "${{ github.event.after }}" GH_REPO: "${{ github.repository }}" GH_TOKEN: "${{ secrets.GIT_PAT }}" - AHA_SERVER: "https://labelstudio.aha.io" - AHA_TOKEN: "${{ secrets.AHA_TOKEN }}" - AHA_PRODUCT: "LSDV" - AHA_RN_FIELD: "release_notes" - AHA_FETCH_STRATEGY: "TAG" - AHA_TAG: "LS ${{ steps.create-draft-release.outputs.tag_name }}" - AHA_ADDITIONAL_RELEASES_TAG: "oss" JIRA_SERVER: "${{ vars.JIRA_SERVER }}" JIRA_USERNAME: "${{ secrets.JIRA_USERNAME }}" JIRA_TOKEN: "${{ secrets.JIRA_TOKEN }}" diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml deleted file mode 100644 index 31cd8b65620..00000000000 --- a/.github/workflows/pr-labeler.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: "PR labeler" - -on: - pull_request_target: - types: - - opened - - edited - - reopened - - synchronize - - ready_for_review - branches: - - master - - develop - - 'release/**' - - 'lse-release/**' - - 'ls-release/**' - -env: - ACTIONS_STEP_DEBUG: '${{ secrets.ACTIONS_STEP_DEBUG }}' - -jobs: - autolabel: - name: "PR label validator" - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - - uses: hmarr/debug-action@v3.0.0 - - - name: "Validate PR's title" - uses: thehanimo/pr-title-checker@v1.4.2 - with: - GITHUB_TOKEN: ${{ github.token }} - pass_on_octokit_error: false - configuration_path: ".github/pr-title-checker-config.json" - - - name: "Set PR's label based on title" - uses: release-drafter/release-drafter@v6.0.0 - with: - disable-releaser: true - config-name: autolabeler.yml - env: - GITHUB_TOKEN: ${{ github.token }} - - release_field: - name: "Set Aha! Releases field" - runs-on: ubuntu-latest - steps: - - uses: hmarr/debug-action@v3.0.0 - - - name: Set field - shell: bash - env: - COMMIT_MESSAGE: ${{ github.event.pull_request.title }} - AHA_SERVER: "https://labelstudio.aha.io" - AHA_TOKEN: "${{ secrets.AHA_TOKEN }}" - AHA_RELEASES_FIELD: releases - TAGS: '["oss"]' - run: | - set -euo pipefail ${ACTIONS_STEP_DEBUG:+-x} - - regex="([a-zA-Z]+):[[:space:]]*([a-zA-Z0-9-]+)?:" - if [[ $COMMIT_MESSAGE =~ $regex ]]; then - export TICKET="${BASH_REMATCH[2]}" - if curl -fsSL \ - --request GET \ - --url "${AHA_SERVER}/api/v1/features/${TICKET}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header 'Accept: application/json'; then - cur_tags=$(curl -sSL \ - --request GET \ - --url "${AHA_SERVER}/api/v1/features/${TICKET}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header "Content-Type: application/json" \ - --header 'Accept: application/json' | - jq -ecr '[ .feature.custom_fields[] | select(.key=="releases") | .value ]') - echo "Current tags: ${cur_tags}" - new_tags=$(jq -ecr ". += ${TAGS} | flatten | unique" <<< $cur_tags) - echo "New tags: ${new_tags}" - curl -sSL \ - --request PUT \ - --url "${AHA_SERVER}/api/v1/features/${TICKET}" \ - --data "{\"feature\":{\"custom_fields\":{\"releases\":${new_tags}}}}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header "Content-Type: application/json" \ - --header 'Accept: application/json' - elif curl -fsSL \ - --request GET \ - --url "${AHA_SERVER}/api/v1/requirements/${TICKET}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header 'Accept: application/json'; then - cur_tags=$(curl -sSL \ - --request GET \ - --url "${AHA_SERVER}/api/v1/requirements/${TICKET}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header "Content-Type: application/json" \ - --header 'Accept: application/json' | - jq -ecr '[ .requirement.custom_fields[] | select(.key=="releases") | .value ]') - echo "Current tags: ${cur_tags}" - new_tags=$(jq -ecr ". += ${TAGS} | flatten | unique" <<< $cur_tags) - echo "New tags: ${new_tags}" - curl -sSL \ - --request PUT \ - --url "${AHA_SERVER}/api/v1/requirements/${TICKET}" \ - --data "{\"requirement\":{\"custom_fields\":{\"releases\":${new_tags}}}}" \ - --header "Authorization: Bearer ${AHA_TOKEN}" \ - --header "Content-Type: application/json" \ - --header 'Accept: application/json' - fi - fi diff --git a/.github/workflows/validator-pull-request-labeler.yml b/.github/workflows/validator-pull-request-labeler.yml new file mode 100644 index 00000000000..dfdb774ac8d --- /dev/null +++ b/.github/workflows/validator-pull-request-labeler.yml @@ -0,0 +1,44 @@ +name: "Check" + +on: + pull_request_target: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + branches: + - master + - develop + - 'release/**' + - 'lse-release/**' + - 'ls-release/**' + +env: + ACTIONS_STEP_DEBUG: '${{ secrets.ACTIONS_STEP_DEBUG }}' + +jobs: + autolabel: + name: "PR label validator" + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + + - uses: hmarr/debug-action@v3.0.0 + + - name: "Validate PR's title" + uses: thehanimo/pr-title-checker@v1.4.2 + with: + GITHUB_TOKEN: ${{ github.token }} + pass_on_octokit_error: false + configuration_path: ".github/pr-title-checker-config.json" + + - name: "Set PR's label based on title" + uses: release-drafter/release-drafter@v6.0.0 + with: + disable-releaser: true + config-name: autolabeler.yml + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/label_studio/feature_flags.json b/label_studio/feature_flags.json index 81f00dbac65..66122b6dbbc 100644 --- a/label_studio/feature_flags.json +++ b/label_studio/feature_flags.json @@ -138,7 +138,7 @@ }, "feat_front_dev_4008_quick_task_open_short": { "key": "feat_front_dev_4008_quick_task_open_short", - "on": false, + "on": true, "prerequisites": [], "targets": [], "contextTargets": [], @@ -160,7 +160,7 @@ "trackEvents": false, "trackEventsFallthrough": false, "debugEventsUntilDate": null, - "version": 2, + "version": 3, "deleted": false }, "feat_front_lsdv_4583_multi_image_segmentation_short": { @@ -595,34 +595,6 @@ "version": 2, "deleted": false }, - "ff_click_new_project_action_short": { - "key": "ff_click_new_project_action_short", - "on": true, - "prerequisites": [], - "targets": [], - "contextTargets": [], - "rules": [], - "fallthrough": { - "variation": 0 - }, - "offVariation": 0, - "variations": [ - "Control", - "Treatment 1", - "Treatment 2" - ], - "clientSideAvailability": { - "usingMobileKey": false, - "usingEnvironmentId": false - }, - "clientSide": false, - "salt": "32c224eac7a14fe5a8ee046dd0a00619", - "trackEvents": false, - "trackEventsFallthrough": false, - "debugEventsUntilDate": null, - "version": 3, - "deleted": false - }, "ff_dev_2007_dev_2008_dynamic_tag_children_250322_short": { "key": "ff_dev_2007_dev_2008_dynamic_tag_children_250322_short", "on": true, @@ -1752,7 +1724,7 @@ "contextTargets": [], "rules": [], "fallthrough": { - "variation": 1 + "variation": 0 }, "offVariation": 1, "variations": [ @@ -1768,7 +1740,7 @@ "trackEvents": false, "trackEventsFallthrough": false, "debugEventsUntilDate": null, - "version": 7, + "version": 8, "deleted": false }, "fflag_feat_all_lsdv_e_295_project_level_roles_via_saml_scim_ldap_short": { diff --git a/label_studio/ml/api.py b/label_studio/ml/api.py index a04f5746a22..f82f78e0edd 100644 --- a/label_studio/ml/api.py +++ b/label_studio/ml/api.py @@ -88,6 +88,15 @@ def perform_create(self, serializer): ml_backend = serializer.save() ml_backend.update_state() + project = ml_backend.project + + # In case we are adding the model, let's set it as the default + # to obtain predictions. This approach is consistent with uploading + # offline predictions, which would be set automatically. + if project.show_collab_predictions and not project.model_version: + project.model_version = ml_backend.title + project.save(update_fields=['model_version']) + @method_decorator( name='patch', diff --git a/label_studio/projects/serializers.py b/label_studio/projects/serializers.py index 3d59467c6ee..ca382b07848 100644 --- a/label_studio/projects/serializers.py +++ b/label_studio/projects/serializers.py @@ -94,6 +94,7 @@ def to_internal_value(self, data): # FIXME: remake this logic with start_training_on_annotation_update initial_data = data data = super().to_internal_value(data) + if 'start_training_on_annotation_update' in initial_data: data['min_annotations_to_start_training'] = int(initial_data['start_training_on_annotation_update']) @@ -186,7 +187,7 @@ def validate_model_version(self, value): return value def update(self, instance, validated_data): - if not validated_data.get('show_collab_predictions'): + if validated_data.get('show_collab_predictions') is False: instance.model_version = '' return super().update(instance, validated_data) diff --git a/label_studio/tests/ml/test_api.py b/label_studio/tests/ml/test_api.py index eece364782a..bb4c1ce28a6 100644 --- a/label_studio/tests/ml/test_api.py +++ b/label_studio/tests/ml/test_api.py @@ -6,6 +6,10 @@ from label_studio.tests.utils import make_project, register_ml_backend_mock +ORIG_MODEL_NAME = 'basic_ml_backend' +PROJECT_CONFIG = """""" + @pytest.fixture def ml_backend_for_test_api(ml_backend): @@ -22,13 +26,72 @@ def mock_gethostbyname(mocker): mocker.patch('socket.gethostbyname', return_value='321.21.21.21') +@pytest.mark.django_db +def test_ml_backend_set_for_prelabeling(business_client, ml_backend_for_test_api, mock_gethostbyname): + project = make_project( + config=dict( + is_published=True, + label_config=PROJECT_CONFIG, + title='test_ml_backend_creation', + ), + user=business_client.user, + ) + + assert project.model_version == '' + + # create ML backend + response = business_client.post( + '/api/ml/', + data={ + 'project': project.id, + 'title': 'ml_backend_title', + 'url': 'https://ml_backend_for_test_api', + }, + ) + assert response.status_code == 201 + + project.refresh_from_db() + assert project.model_version == 'ml_backend_title' + + +@pytest.mark.django_db +def test_ml_backend_not_set_for_prelabeling(business_client, ml_backend_for_test_api, mock_gethostbyname): + """We are not setting it when its already set for another name, + for example when predictions were uploaded before""" + + project = make_project( + config=dict( + is_published=True, + label_config=PROJECT_CONFIG, + title='test_ml_backend_creation', + ), + user=business_client.user, + ) + + project.model_version = ORIG_MODEL_NAME + project.save() + + # create ML backend + response = business_client.post( + '/api/ml/', + data={ + 'project': project.id, + 'title': 'ml_backend_title', + 'url': 'https://ml_backend_for_test_api', + }, + ) + assert response.status_code == 201 + + project.refresh_from_db() + assert project.model_version == ORIG_MODEL_NAME + + @pytest.mark.django_db def test_model_version_on_save(business_client, ml_backend_for_test_api, mock_gethostbyname): project = make_project( config=dict( is_published=True, - label_config="""""", + label_config=PROJECT_CONFIG, title='test_ml_backend_creation', ), user=business_client.user, @@ -86,8 +149,7 @@ def test_model_version_on_delete(business_client, ml_backend_for_test_api, mock_ project = make_project( config=dict( is_published=True, - label_config="""""", + label_config=PROJECT_CONFIG, title='test_ml_backend_creation', ), user=business_client.user, @@ -135,8 +197,7 @@ def test_security_write_only_payload(business_client, ml_backend_for_test_api, m project = make_project( config=dict( is_published=True, - label_config="""""", + label_config=PROJECT_CONFIG, title='test_ml_backend_creation', ), user=business_client.user, @@ -226,8 +287,7 @@ def test_ml_backend_predict_test_api_post_random_true(business_client): project = make_project( config=dict( is_published=True, - label_config="""""", + label_config=PROJECT_CONFIG, title='test_ml_backend_creation', ), user=business_client.user, diff --git a/label_studio/tests/predictions.model.tavern.yml b/label_studio/tests/predictions.model.tavern.yml index 9df751b035d..6545721caa8 100644 --- a/label_studio/tests/predictions.model.tavern.yml +++ b/label_studio/tests/predictions.model.tavern.yml @@ -136,7 +136,7 @@ stages: request: data: project: '{project_pk}' - title: My Testing ML backend + title: "ml_backend" url: https://test.heartex.mlbackend.com:9090 method: POST url: '{django_live_url}/api/ml' @@ -147,25 +147,7 @@ stages: response: status_code: 200 json: - model_version: '' -- name: change_project_model_to_ml_backend - request: - method: PATCH - url: '{django_live_url}/api/projects/{project_pk}' - data: - model_version: "My Testing ML backend" - response: - status_code: 200 - json: - model_version: "My Testing ML backend" -- name: check_project_model_after_project_change - request: - method: GET - url: '{django_live_url}/api/projects/{project_pk}' - response: - status_code: 200 - json: - model_version: "My Testing ML backend" + model_version: "ml_backend" --- test_name: model_change_before_ML_added @@ -305,15 +287,15 @@ stages: request: data: project: '{project_pk}' - title: My Testing ML backend + title: "ml_backend" url: https://test.heartex.mlbackend.com:9090 method: POST url: '{django_live_url}/api/ml' -- name: check_project_model_not_change_after_ml_added_to_empty +- name: check_project_model_change_after_ml_added_to_empty request: method: GET url: '{django_live_url}/api/projects/{project_pk}' response: status_code: 200 json: - model_version: "" + model_version: "ml_backend" diff --git a/web/apps/labelstudio/src/components/Menubar/Menubar.jsx b/web/apps/labelstudio/src/components/Menubar/Menubar.jsx index bbd0f3846cf..4b1f27d7e63 100644 --- a/web/apps/labelstudio/src/components/Menubar/Menubar.jsx +++ b/web/apps/labelstudio/src/components/Menubar/Menubar.jsx @@ -233,12 +233,14 @@ export const Menubar = ({ href="https://github.com/heartexlabs/label-studio" icon={} target="_blank" + rel="noreferrer" /> } target="_blank" + rel="noreferrer" /> diff --git a/web/apps/labelstudio/src/components/VersionNotifier/VersionNotifier.jsx b/web/apps/labelstudio/src/components/VersionNotifier/VersionNotifier.jsx index bc1a0a2b280..fa4d05bd9fe 100644 --- a/web/apps/labelstudio/src/components/VersionNotifier/VersionNotifier.jsx +++ b/web/apps/labelstudio/src/components/VersionNotifier/VersionNotifier.jsx @@ -52,7 +52,7 @@ export const VersionNotifier = ({ showNewVersion, showCurrentVersion }) => { return (newVersion && showNewVersion) ? ( - + diff --git a/web/apps/labelstudio/src/pages/ExportPage/ExportPage.jsx b/web/apps/labelstudio/src/pages/ExportPage/ExportPage.jsx index 4a41b030f9d..db350f9f147 100644 --- a/web/apps/labelstudio/src/pages/ExportPage/ExportPage.jsx +++ b/web/apps/labelstudio/src/pages/ExportPage/ExportPage.jsx @@ -235,7 +235,13 @@ const FormatInfo = ({ availableFormats, selected, onClick }) => { Can't find an export format?
- Please let us know in
Slack or submit an issue to the Repository + Please let us know in + {" "} + Slack + {" "} + or submit an issue to the + {" "} + Repository
); diff --git a/web/apps/labelstudio/src/pages/Organization/PeoplePage/PeoplePage.jsx b/web/apps/labelstudio/src/pages/Organization/PeoplePage/PeoplePage.jsx index bb05e3e278f..851d2ebe48b 100644 --- a/web/apps/labelstudio/src/pages/Organization/PeoplePage/PeoplePage.jsx +++ b/web/apps/labelstudio/src/pages/Organization/PeoplePage/PeoplePage.jsx @@ -26,7 +26,7 @@ const InvitationModal = ({ link }) => { /> - Invite people to join your Label Studio instance. People that you invite have full access to all of your projects. Learn more. + Invite people to join your Label Studio instance. People that you invite have full access to all of your projects. Learn more. ); diff --git a/web/apps/labelstudio/src/pages/Settings/AnnotationSettings.jsx b/web/apps/labelstudio/src/pages/Settings/AnnotationSettings.jsx index 6aecc6969d2..fc1b719b11f 100644 --- a/web/apps/labelstudio/src/pages/Settings/AnnotationSettings.jsx +++ b/web/apps/labelstudio/src/pages/Settings/AnnotationSettings.jsx @@ -57,23 +57,13 @@ export const AnnotationSettings = () => {
- Live Predictions + Prelabeling
- Enable and select which set of predictions to use for prelabeling. - Predictions will be pre-loaded in {" "} - Label All Tasks{" "} - and {" "} - Quick View. + Enable and select which set of predictions to use for prelabeling. } name="show_collab_predictions" diff --git a/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/MachineLearningList.jsx b/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/MachineLearningList.jsx index c41404cf9e5..3f52471fe63 100644 --- a/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/MachineLearningList.jsx +++ b/web/apps/labelstudio/src/pages/Settings/MachineLearningSettings/MachineLearningList.jsx @@ -85,11 +85,11 @@ const BackendCard = ({
- onEdit(backend)}>Edit - confirmDelete(backend)} isDangerous>Delete - + onEdit(backend)}>Edit onTestRequest(backend)}>Send Test Request onStartTrain(backend)}>Start Training + + confirmDelete(backend)} isDangerous>Delete )}>