Class ::github::ActivityMonitor

::github::ActivityMonitor[i] create ... \
           [ -api_base (default "https://api.github.com") ] \
           [ -api_token api_token ] \
           [ -organization (default "OpenACS") ]

Defined in packages/xowiki/tcl/github-activity-monitor-procs.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nx::Object[i]
::nx::Class create ::github::ActivityMonitor \
     -superclass ::nx::Object

Methods (to be applied on instances)

  • backfill_repo_history (scripted, public)

     <instance of github::ActivityMonitor[i]> backfill_repo_history \
        [ -repo repo ] [ -branch branch ] [ -start_page start_page ] \
        [ -max_pages max_pages ]

    Backfill commit history for a single repo/branch using /repos/{repo}/commits. Uses synthetic negative event_id values. repo e.g. "openacs/openacs-core" branch e.g. "main" or "oacs-5-10"

    Switches:
    -repo (optional)
    -branch (optional, defaults to "main")
    -start_page (optional, defaults to "1")
    -max_pages (optional, defaults to "50")

    Testcases:
    No testcase defined.
    ns_log Notice "GitHub backfill_repo_history: $repo ($branch)"
    
    # We’ll assign event_id from next negative downwards.
    set next_event_id [:next_backfill_event_id]
    
    set page $start_page
    set pages_done 0
    set done 0            
    set count 0
    
    while {!$done} {
        if {$pages_done > $max_pages} {
            ns_log notice "GitHub backfill_repo_history: reached"  "max_pages=$max_pages for $repo ($branch)"
            break
        }
    
        # List commits, newest first
        set body [:get "/repos/$repo/commits" [list sha $branch page $page per_page 100]]
        set batch [::util::json2dict $body]
    
        if {[llength $batch] == 0} {
            ns_log notice "GitHub backfill_repo_history: no more commits"  at page $page for $repo ($branch)
            break
        }
    
        ::xo::dc transaction {
            foreach summary $batch {
                set sha [dict get $summary sha]
    
                # Stop if we already have this commit (from events or prior backfill)
                set exists [xo::dc 0or1row exists_commit {
                    select 1 from github_activity where repo = :repo and sha = :sha limit 1
                }]
    
                if {$exists} {
                    # We assume older pages are fully covered as well
                    ns_log notice "GitHub backfill_repo_history: found existing commit"  "$repo $sha, stopping at page $page"
                    #set done 1
                    #break
                    continue
                }
    
                incr count
                
                #
                # Fetch full commit details (stats, files, etc.),
                # similar to summarize_push_event.
                #
                set commit_body   [:get "/repos/$repo/commits/$sha"]                        
                set c             [::util::json2dict $commit_body]
                set commit        [dict get $c commit]
                
                #ns_log notice "commit keys: [dict keys $commit]"
                ns_log notice "(page $page, count $count) commit.message($count):"  [dict get $commit message]
                
                set author_dict   [dict get $commit author]
                set author_name   [dict get $author_dict name]
                set commit_date   [dict get $author_dict date]
                set stats         [dict get $c stats]
                set additions     [dict get $stats additions]
                set deletions     [dict get $stats deletions]
                set files         [dict get $c files]
                set files_changed [llength $files]
    
                set message_full  [dict get $commit message]
    
                if {$message_full eq ""} {
                    # Fallback: don’t leave title NULL, DB wants NOT NULL
                    set title "(no commit message)"
                } else {
                    set title [lindex [split [string trimleft $message_full] \n] 0]
                }
                #ns_log notice "commit.title: $title"
                
                if {[dict exists $c html_url]} {
                    set url [dict get $c html_url]
                } else {
                    # construct GitHub web URL as a fallback
                    set url "https://github.com/$repo/commit/$sha"
                }                        
                
                # Synthetic negative event_id
                set event_id $next_event_id
                incr next_event_id -1
                
                set created_at $commit_date   ;# for backfill, we just reuse commit time
                
                ::xo::dc dml insert_backfill {
                    insert into github_activity (
                                                 event_id, repo, branch, sha,
                                                 author_name, author_login,
                                                 commit_date, created_at,
                                                 title, url,
                                                 files_changed, additions, deletions,
                                                 raw_payload
                                                 ) values (
                                                           :event_id, :repo, :branch, :sha,
                                                           :author_name, NULL,
                                                           :commit_date, :created_at,
                                                           :title, :url,
                                                           :files_changed, :additions, :deletions,
                                                           :commit_body
                                                           )
                    on conflict (repo, sha) do nothing
                }
            }
        }
    
        if {$done} {
            break
        }
    
        incr page
        incr pages_done
    }
    
    ns_log notice "GitHub backfill_repo_history: done for $repo ($branch)"
  • get (scripted, public)

     <instance of github::ActivityMonitor[i]> get path [ query_args ]

    Issue an API call on GitHub for the configured organization.

    Parameters:
    path (required)
    query_args (optional)

    Testcases:
    No testcase defined.
    set url "${:api_base}${path}"
    
    # default query arg
    dict set query per_page 100
    foreach {k v} $query_args {
        dict set query $k $v
    }
    set urlDict [ns_parseurl $url]
    dict set urlDict query [join [lmap {k v} $query {string cat $k=$v}] &]
    
    set headers [ns_set create headers  Authorization "Bearer ${:api_token}"  Accept        "application/vnd.github+json"  User-Agent    "openacs-activity-dashboard/1.0"  ]
    
    ns_log notice request: [list ns_http run -method GET -headers $headers [ns_joinurl $urlDict]]
    #error 1
    set result [ns_http run -method GET -headers $headers [ns_joinurl $urlDict]]
    set status [dict get $result status]
    set body   [dict get $result body]
    
    if {$status < 200 || $status >= 300} {
        ns_log Error "GitHub GET $url failed: status $status, body: $body"
        error "GitHub API error $status"
    }
    return $body
  • summarize_push_event (scripted, public)

     <instance of github::ActivityMonitor[i]> summarize_push_event ev

    Fetch commit details and build a summary for PushEvents.

    Parameters:
    ev (required)
    Returns:
    dict with fields of push event

    Testcases:
    No testcase defined.
    set type [dict get $ev type]
    if {$type ne "PushEvent"} {
        return ""
    }
    
    set repo_full [dict get $ev repo name]          ;# e.g. "openacs/openacs-core"
    set ref       [dict get $ev payload ref]        ;# "refs/heads/main"
    set branch    [lindex [split $ref "/"] end]
    set head_sha  [dict get $ev payload head]
    set created_at [dict get $ev created_at]
    set actor_login [dict get $ev actor login]
    
    if {$repo_full eq "" || $head_sha eq ""} {
        return ""
    }
    
    # Fetch commit details
    set commit_body [:get "/repos/$repo_full/commits/$head_sha"]
    set c          [::util::json2dict $commit_body]
    set commit     [dict get $c commit]
    
    set author_dict [dict get $commit author]
    set author_name [dict get $author_dict name]
    set commit_date [dict get $author_dict date]    ;# ISO8601 string
    
    set message_full [dict get $commit message]
    set title        [lindex [split $message_full "\n"] 0]
    
    set stats        [dict get $c stats]
    set additions    [dict get $stats additions]
    set deletions    [dict get $stats deletions]
    set files        [dict get $c files]
    set files_changed [llength $files]
    
    return [list  event_id      [dict get $ev id]  repo          $repo_full  branch        $branch  sha           [dict get $c sha]  author_name   $author_name  author_login  $actor_login  commit_date   $commit_date  created_at    $created_at  title         $title  url           [dict get $c html_url]  files_changed $files_changed  additions     $additions  deletions     $deletions  raw_payload   $commit_body  ]
  • sync_from_github (scripted, public)

     <instance of github::ActivityMonitor[i]> sync_from_github

    Sync events from github: fetch, summarize, insert

    Testcases:
    No testcase defined.
    ns_log Notice "GitHub: refresh_activity start"
    
    set new_events [:fetch_new_events]
    if {[llength $new_events] == 0} {
        ns_log Notice "GitHub: no new events"
        return
    }
    
    ::xo::dc transaction {
        foreach ev $new_events {
            set summary [:summarize_push_event $ev]
            if {$summary eq ""} {
                continue
            }
    
            dict with summary {
                # Avoid duplicate insert if raced with another run
                xo::dc dml insert_activity {
                    insert into github_activity (
                                                 event_id, repo, branch, sha,
                                                 author_name, author_login,
                                                 commit_date, created_at,
                                                 title, url,
                                                 files_changed, additions, deletions,
                                                 raw_payload
                                                 ) values (
                                                           :event_id, :repo, :branch, :sha,
                                                           :author_name, :author_login,
                                                           :commit_date, :created_at,
                                                           :title, :url,
                                                           :files_changed, :additions, :deletions,
                                                           :raw_payload
                                                           )
                    on conflict (repo, sha) do nothing
                }
            }
        }
    }
    ns_log Notice "GitHub: refresh_activity done; processed [llength $new_events] events"