]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
.github/workflows: add create-backport action
authorErnesto Puerta <37327689+epuertat@users.noreply.github.com>
Fri, 5 Aug 2022 08:56:36 +0000 (10:56 +0200)
committerErnesto Puerta <epuertat@redhat.com>
Wed, 10 Aug 2022 11:09:52 +0000 (13:09 +0200)
Currently there's a cron job in a teuthology VM running a script to find all trackers in needs-backports and to create their corresponding backport trackers. This is done through the [Backport Bot](https://tracker.ceph.com/users/12172) Redmine account.

This PR intends to run this cron job task as a periodic Github Action.

Signed-off-by: Ernesto Puerta <37327689+epuertat@users.noreply.github.com>
Signed-off-by: Ernesto Puerta <epuertat@redhat.com>
.github/workflows/create-backport-trackers.yml [new file with mode: 0644]
src/script/backport-create-issue
src/script/requirements.backport-create-issue.txt [new file with mode: 0644]

diff --git a/.github/workflows/create-backport-trackers.yml b/.github/workflows/create-backport-trackers.yml
new file mode 100644 (file)
index 0000000..b3525d9
--- /dev/null
@@ -0,0 +1,50 @@
+---
+name: Create backport trackers for trackers in "Pending Backport" state
+on:
+  # To manually trigger this: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
+  workflow_dispatch:
+    inputs:
+      issues:
+        description: 'whitespace-separated list of issue numbers'
+        type: string
+        default: ''      
+      debug:
+        description: '--debug: Show debug-level messages'
+        default: false
+        type: boolean
+      resolveParent:
+        description: '--resolve-parent: Resolve parent issue if all backports resolved/rejected'
+        default: false
+        type: boolean
+      force:
+        description: >
+          --force: When issue numbers provided, process them even if not in
+          'Pending Backport' status.
+          Otherwise, process all issues in 'Pending Backport' status even if
+          already processed (tag 'backport_processed' added)'
+        default: false
+        type: boolean
+      dryRun:
+        description: '--dry-run: Do not write anything to Redmine'
+        default: false
+        type: boolean
+  schedule:
+    # Every 5 minutes: https://crontab.guru/every-5-minutes
+    - cron: '*/5 * * * *'
+jobs:
+  create-backports:
+    runs-on: ubuntu-latest
+    if: github.ref == 'refs/heads/main'
+    steps:
+      - uses: Bhacaz/checkout-files@e3e34e7daef91a5f237485bb88a260aee4be29dd
+        with:
+          files: src/script/backport-create-issue src/script/requirements.backport-create-issue.txt
+      - uses: actions/setup-python@v4
+        with:
+          python-version: '>=3.6'
+          cache: 'pip'
+          cache-dependency-path: src/script/requirements.backport-create-issue.txt
+      - run: pip install -r src/script/requirements.backport-create-issue.txt
+      - run: python3 src/script/backport-create-issue ${{ inputs.debug && '--debug' || '' }} ${{ inputs.resolveParent && '--resolve-parent' || '' }} ${{ inputs.force && '--force' || '' }} ${{ inputs.dryRun && '--dry-run' || '' }} ${{ inputs.issues }}
+        env:
+          REDMINE_API_KEY: ${{ secrets.REDMINE_API_KEY_BACKPORT_BOT }}
index 8336ac29348fc4b16c8ec17a7a6bacf617b95650..e2e2298f977fb5e406b561ada145c406b190ba93 100755 (executable)
@@ -39,8 +39,12 @@ from redminelib.exceptions import ResourceAttrError
 redmine_endpoint = "https://tracker.ceph.com"
 project_name = "Ceph"
 release_id = 16
+custom_field_tag = 'cf_3'
+tag_separator = ' '
+tag_backport_processed = 'backport_processed'
 delay_seconds = 5
 redmine_key_file="~/.redmine_key"
+redmine_key_env="REDMINE_API_KEY"
 #
 # NOTE: release_id is hard-coded because
 # http://www.redmine.org/projects/redmine/wiki/Rest_CustomFields
@@ -60,11 +64,12 @@ resolve_parent = None
 
 def usage():
     logging.error("Redmine credentials are required to perform this operation. "
-                  "Please provide either a Redmine key (via %s) "
+                  "Please provide either a Redmine key (via %s or $%s) "
                   "or a Redmine username and password (via --user and --password). "
                   "Optionally, one or more issue numbers can be given via positional "
                   "argument(s). In the absence of positional arguments, the script "
-                  "will loop through all issues in Pending Backport status." % redmine_key_file)
+                  "will loop through all issues in Pending Backport status.",
+                  redmine_key_file, redmine_key_env)
     exit(-1)
 
 def parse_arguments():
@@ -78,10 +83,15 @@ def parse_arguments():
                         action="store_true")
     parser.add_argument("--dry-run", help="Do not write anything to Redmine",
                         action="store_true")
-    parser.add_argument("--force", help="Create backport issues even if status not Pending Backport",
+    parser.add_argument("--force", help="When issue numbers provided, process "
+                        "them even if not in 'Pending Backport' status. "
+                        "Otherwise, process all issues in 'Pending Backport' "
+                        "status even if already processed "
+                        f"(tag '{tag_backport_processed}' added)",
                         action="store_true")
     return parser.parse_args()
 
+
 def set_logging_level(a):
     if a.debug:
         logging.basicConfig(level=logging.DEBUG)
@@ -117,6 +127,9 @@ def connect_to_redmine(a):
     elif redmine_key:
         logging.info("Redmine key was read from '%s'; using it" % redmine_key_file)
         return Redmine(redmine_endpoint, key=redmine_key)
+    elif os.getenv(redmine_key_env):
+        logging.info("Redmine key was read from '$%s'; using it", redmine_key_env)
+        return Redmine(redmine_endpoint, key=os.getenv(redmine_key_env))
     else:
         usage()
 
@@ -285,6 +298,27 @@ def maybe_resolve(issue, backports, dry_run):
     else:
         logging.debug("Some backport issues are still unresolved: leaving parent issue open")
 
+
+def mark_as_processed(r, issue):
+    """
+    This script will add a custom Tag to indicate whether the tracker was
+    already processed for backport tracker creation.
+    """
+    custom_fields = list(issue['custom_fields'].values())
+    for i, field in enumerate(custom_fields):
+        if field['name'] == 'Tags':
+            if tag_backport_processed not in field['value']:
+                if field['value']:
+                    custom_fields[i]['value'] += (tag_separator +
+                                                  tag_backport_processed)
+                else:
+                    custom_fields[i]['value'] = tag_backport_processed
+                logging.info("%s adding tag '%s'", url(issue),
+                             tag_backport_processed)
+                r.issue.update(issue.id, custom_fields=custom_fields)
+                return
+
+
 def iterate_over_backports(r, issues, dry_run=False):
     counter = 0
     for issue in issues:
@@ -303,6 +337,8 @@ def iterate_over_backports(r, issues, dry_run=False):
         if len(issue['backports']) == 0:
             logging.error(url(issue) + " the backport field is empty")
         update_relations(r, issue, dry_run)
+        if not dry_run:
+            mark_as_processed(r, issue)
     print('                                     \r', end='', flush=True)
     logging.info("Processed {} issues".format(counter))
     return None
@@ -337,10 +373,20 @@ if __name__ == '__main__':
                                           issue_id=issue_list,
                                           status_id=pending_backport_status_id)
     else:
-        if args.force:
-            logging.warn("ignoring --force option, which can only be used with an explicit issue list")
-        issues = redmine.issue.filter(project_id=ceph_project_id,
-                                      status_id=pending_backport_status_id)
+        if args.force or args.resolve_parent:
+            if args.force:
+                logging.warn("--force option was given: ignoring '%s' tag!",
+                             tag_backport_processed)
+            issues = redmine.issue.filter(project_id=ceph_project_id,
+                                          status_id=pending_backport_status_id)
+        else:
+            # https://python-redmine.com/resources/issue.html#filter
+            issues = redmine.issue.filter(project_id=ceph_project_id,
+                                          status_id=pending_backport_status_id,
+                                          **{
+                                              custom_field_tag:
+                                              '!~' +
+                                              tag_backport_processed})
     if force_create:
         logging.info("Processing {} issues regardless of status"
                      .format(len(issues)))
diff --git a/src/script/requirements.backport-create-issue.txt b/src/script/requirements.backport-create-issue.txt
new file mode 100644 (file)
index 0000000..832772f
--- /dev/null
@@ -0,0 +1 @@
+python-redmine == 2.3.0