Skip to content

feat: ✨ Major updates and bug fixes#181

Merged
slugb0t merged 4 commits intomainfrom
staging
Feb 23, 2026
Merged

feat: ✨ Major updates and bug fixes#181
slugb0t merged 4 commits intomainfrom
staging

Conversation

@slugb0t
Copy link
Member

@slugb0t slugb0t commented Feb 23, 2026

Summary by Sourcery

Refactor the bot’s event handling into dedicated handler modules, enhance FAIR archival and license validation logic using metadata-derived identifiers and SPDX-aware checks, and improve the license editing UI/flow along with minor build and Docker configuration updates.

New Features:

  • Add metadata-driven DOI detection and badge rendering for FAIR archival checks, including support for Zenodo-specific and other DOI formats.
  • Introduce dedicated Probot handlers for installation, push, pull request, and issue events to modularize and centralize bot behavior.
  • Add a confirmation workflow in the license editor UI to distinguish between confirming an existing license and switching to a new one.

Bug Fixes:

  • Ensure dashboard re-rendering uses structured license status from the database and corrects code of conduct detection property names.
  • Prevent loss or misclassification of license information when GitHub cannot automatically identify the license, including in pending-PR scenarios.
  • Fix DOI badge rendering by properly encoding labels for shields.io and avoiding malformed badge URLs.

Enhancements:

  • Improve archival template logic to prioritize existing Codefair Zenodo releases, then fall back to DOIs discovered in codemeta.json and CITATION.cff, with clearer messaging for multiple or non-Zenodo identifiers.
  • Normalize license content before comparison and preserve valid SPDX IDs in the database to better handle content-only changes.
  • Use shared helpers for issue titles and PR titles to avoid duplication and improve consistency across handlers.

Build:

  • Update bot and validator Dockerfiles to include new handler and schema files and correct copy ordering.
  • Tidy database Docker Compose volume configuration formatting.

* fix: 🐛 docker volume for postgres data

* feat: ✨ Abstract bot event listeners to files

* fix: 🐛 improve SPDX validation and add verification workflow

* feat: ✨ detect existing archivals for fair release status

* refactor: ♻️ Add line break after pr badge in issue dashboard

* fix: 🐛 add await to db call

* refactor: ♻️ Sourcery suggestion

* fix(sourcery): 🐛 Edge case with doi format and badge generation
@fairdataihub-bot
Copy link

Thank you for submitting this pull request! We appreciate your contribution to the project. Before we can merge it, we need to review the changes you've made to ensure they align with our code standards and meet the requirements of the project. We'll get back to you as soon as we can with feedback. Thanks again!

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 23, 2026

Reviewer's Guide

This PR refactors the Probot bot into modular event handlers, enhances FAIR archival and licensing logic (both backend and dashboard UI) including DOI/Zenodo handling and SPDX-aware license validation, and updates Docker and validator assets to support new behavior and schemas.

Sequence diagram for applyArchivalTemplate with metadata DOI detection

sequenceDiagram
  participant GH as GitHub
  participant Bot as Probot_app
  participant Handler as issues_reopened_handler
  participant RC as runComplianceChecks
  participant Renderer as renderIssues
  participant Arch as applyArchivalTemplate
  participant Meta as metadata_identifier_helpers
  participant DB as Prisma_db
  participant Zenodo as Zenodo_API

  GH->>Bot: issues.reopened
  Bot->>Handler: handleIssuesReopened(context)
  Handler->>DB: installation.update(disabled=false)
  Handler->>Handler: isRepoEmpty(context, owner, repo)
  alt repo_not_empty
    Handler->>Handler: gatherCommitDetails(context, owner, repo)
  else repo_empty
    Handler->>Handler: createEmptyCommitInfo()
  end
  Handler->>DB: verifyInstallationAnalytics(context, repo, 0, latestCommitInfo)
  Handler->>RC: runComplianceChecks(context, owner, repo)
  RC-->>Handler: subjects
  Handler->>Renderer: renderIssues(context, owner, repo, emptyRepo, subjects)
  activate Renderer
  Renderer->>Arch: applyArchivalTemplate(context, baseTemplate, repo, owner, subjects)
  activate Arch
  Arch->>DB: zenodoDeposition.findUnique(repository_id)
  alt existing_Codefair_release_with_doi
    Arch->>DB: zenodoDeposition.update(existing_zenodo_deposition_id=true)
    Arch-->>Renderer: baseTemplate_with_Zenodo_badge
  else no_Codefair_release_or_no_doi
    Arch->>Meta: fetchAndExtractIdentifiers(context, owner, repo)
    activate Meta
    Meta->>Meta: getCodemetaContent(context, owner, repo)
    Meta->>Meta: extractIdentifiersFromCodemeta(codemetaContent)
    Meta->>Meta: getCitationContent(context, owner, repo)
    Meta->>Meta: extractIdentifiersFromCitation(citationContent)
    Meta->>Meta: prioritizeIdentifiers(identifiers)
    Meta-->>Arch: primary_identifier, other_identifiers
    alt no_identifiers
      Arch-->>Renderer: baseTemplate_with_first_release_cta
    else single_identifier
      Arch-->>Renderer: baseTemplate_with_single_identifier_section
    else multiple_identifiers
      Arch-->>Renderer: baseTemplate_with_multiple_identifiers_section
    end
  end
  deactivate Arch
  Renderer-->>Handler: final_issue_body
  deactivate Renderer
  Handler->>GH: createIssue(context, owner, repo, ISSUE_TITLE, issueBody)
  GH-->>Zenodo: later_via_publishToZenodo_on_command
Loading

Class diagram for archival and license validation helpers

classDiagram
  class ArchivalModule {
    +IDENTIFIER_TYPE_ZENODO_DOI
    +IDENTIFIER_TYPE_OTHER_DOI
    +IDENTIFIER_TYPE_NON_DOI
    +extractDOIFromString(value)
    +classifyIdentifier(identifier)
    +extractIdentifiersFromCodemeta(codemetaContent)
    +extractIdentifiersFromCitation(citationContent)
    +fetchAndExtractIdentifiers(context, owner, repository)
    +prioritizeIdentifiers(identifiers)
    +createShieldsLabelFromDOI(doi)
    +createZenodoDOIBadge(doi, zenodoId)
    +createOtherDOIBadge(doi)
    +renderSingleIdentifierTemplate(identifier, releaseBadge, firstReleaseBadge)
    +renderMultipleIdentifiersTemplate(primary, others, releaseBadge, firstReleaseBadge)
    +applyArchivalTemplate(context, baseTemplate, repository, owner, subjects)
  }

  class LicenseModule {
    +isValidSpdxLicense(licenseId)
    +normalizeContent(content)
    +validateLicense(license, existingLicense)
    +updateLicenseDatabase(repository, license)
    +applyLicenseTemplate(baseTemplate, repository, subjects, licenseRow)
  }

  class RendererModule {
    +renderIssues(context, owner, repository, emptyRepo, subjects)
    +createIssue(context, owner, repository, issueTitle, issueBody)
  }

  class ActionsModule {
    +reRenderDashboard(context, owner, repository, issueBody)
    +publishToZenodo(context, owner, repository, issueBody)
  }

  class HelpersModule {
    +ISSUE_TITLE
    +PR_TITLES_license
    +PR_TITLES_metadataAdd
    +PR_TITLES_metadataUpdate
    +createEmptyCommitInfo()
  }

  class InstallationHandlers {
    +registerInstallationHandlers(app, db)
  }

  class PushHandlers {
    +registerPushHandler(app, db)
  }

  class PullRequestHandlers {
    +registerPullRequestHandlers(app, db)
  }

  class IssueHandlers {
    +registerIssueHandlers(app, db)
  }

  class DB_Prismaclient {
    +installation
    +licenseRequest
    +codeMetadata
    +zenodoDeposition
  }

  ArchivalModule --> DB_Prismaclient : uses
  ArchivalModule --> HelpersModule : uses_constants
  LicenseModule --> DB_Prismaclient : uses
  LicenseModule --> ArchivalModule : uses_isValidSpdxLicense
  RendererModule --> ArchivalModule : calls_applyArchivalTemplate
  RendererModule --> LicenseModule : calls_applyLicenseTemplate
  ActionsModule --> RendererModule : uses
  InstallationHandlers --> RendererModule : uses
  InstallationHandlers --> ArchivalModule : uses_applyArchivalTemplate_indirectly
  PushHandlers --> RendererModule : uses
  PullRequestHandlers --> ActionsModule : uses_reRenderDashboard
  IssueHandlers --> ActionsModule : uses
  IssueHandlers --> RendererModule : uses
Loading

File-Level Changes

Change Details Files
Refactor Probot entrypoint into modular event handler modules for installations, pushes, pull requests, and issues.
  • Replace inlined app.on handlers in bot/index.js with calls to registerInstallationHandlers, registerPushHandler, registerPullRequestHandlers, and registerIssueHandlers
  • Introduce handlers/installation.js to manage installation create/add and uninstall/remove events, including initial compliance runs and DB setup using shared helper createEmptyCommitInfo
  • Introduce handlers/push.js to encapsulate push event handling, action_count management, bot-commit ignoring, compliance checks, and dashboard issue updates
  • Introduce handlers/pullRequest.js to manage bot-created PR open/close events, storing/clearing PR URLs in licenseRequest/codeMetadata, re-rendering the dashboard via reRenderDashboard, and deleting feature branches
  • Introduce handlers/issue.js to manage issues.edited/closed/reopened events, including a data-driven trigger router for rerun/publish commands and dashboard lifecycle; use ISSUE_TITLE and createEmptyCommitInfo helpers
  • Add utils/helpers.js providing shared ISSUE_TITLE, PR_TITLES constants and createEmptyCommitInfo used across handlers
bot/index.js
bot/handlers/installation.js
bot/handlers/push.js
bot/handlers/pullRequest.js
bot/handlers/issue.js
bot/utils/helpers.js
bot/Dockerfile
Improve FAIR archival (Zenodo/DOI) handling by introspecting metadata files for identifiers and rendering nuanced dashboard guidance and badges.
  • Add DOI parsing utilities (extractDOIFromString, classifyIdentifier, IDENTIFIER_TYPE constants, DOI regex/prefix) and helper functions to classify identifiers as Zenodo DOIs, other DOIs, or non-DOIs
  • Implement extraction of identifiers from codemeta.json and CITATION.cff using getCodemetaContent/getCitationContent and js-yaml parsing, with de-duplication and prioritization (Zenodo > other DOI > non-DOI)
  • Add shields.io label helper that escapes dashes and URL-encodes DOIs, and badge builders for Zenodo DOIs and other DOIs
  • Introduce template renderers for single and multiple identifiers that generate different FAIR Software Release sections (✔️/❌/ℹ️), notes, and details blocks depending on identifier types and counts
  • Refactor applyArchivalTemplate to accept context first, check for a valid existing Codefair Zenodo deposition, and otherwise consult identifiers from metadata to decide between first-release messaging, Zenodo-based messaging, or "other DOI/non-DOI" guidance with appropriate badges and buttons
  • Expose archival helpers for testing
  • Wire applyArchivalTemplate new signature into renderIssues by passing context
bot/compliance-checks/archival/index.js
bot/compliance-checks/metadata/index.js
bot/utils/renderer/index.js
Tighten license validation logic, tracking, and dashboard messaging, including SPDX-aware comparison and pending-PR behavior.
  • Load SPDX licenses JSON in license compliance module and add isValidSpdxLicense helper to check SPDX IDs while excluding Custom/NOASSERTION
  • Introduce normalizeContent to compare license texts robustly by normalizing whitespace and line endings
  • Enhance validateLicense so when GitHub reports NOASSERTION but content exists, Codefair decides whether to mark the license as Custom for verification, preserve a known SPDX license, or reuse an existing custom ID based on content changes and SPDX validity
  • Update updateLicenseDatabase to preserve license information when the repo lacks a LICENSE file but there is a pending license PR, avoiding losing state during transition
  • Adjust license dashboard text for Custom-without-title to explain that the LICENSE needs verification (content changed/unrecognized) and that SPDX is required for Zenodo automation
  • Update reRenderDashboard to construct a license object (status/content/spdx_id/path) from DB rows instead of a boolean, and fix cofc to read contains_cofc
bot/compliance-checks/license/index.js
bot/commands/actions/index.js
Enhance dashboard license editor UX to support confirming existing content vs fetching a standard template, especially when a license is flagged for verification.
  • Track originalLicenseId from API response to distinguish between verifying a flagged Custom license and changing licenses
  • Replace direct v-model on n-select with controlled :value plus updateLicenseContent handler to intercept selection changes
  • When selecting a new license and existing content is present, show a confirmation modal with flows to keep existing content (confirm and save) or fetch a fresh standard template
  • Implement confirmLicenseKeepContent to set the new licenseId, call the /license/custom_title PUT endpoint with existing content (marking license as non-custom, no title), and show success/error toasts
  • Implement confirmLicenseFetchFresh and cancelLicenseSelection helpers and extract fetchLicenseTemplate to fetch and load a template by licenseId
  • Add canConfirmExistingContent computed to gate the "Confirm and save" option to previously Custom licenses only, and update layout/spacing to accommodate the new modal and messages
ui/pages/dashboard/[owner]/[repo]/edit/license.vue
Misc infrastructure and asset updates (Dockerfiles, validator schemas, minor renderer fix).
  • Adjust bot Dockerfile copy order to include handlers and avoid duplicate commands copy line
  • Update validator Dockerfile to include codemeta-schema2.0.json alongside the existing schema
  • Ensure db-docker-compose.yaml ends with a newline
  • Append extra newline after LICENSE PR badge in renderIssues so dashboard formatting is cleaner
bot/Dockerfile
validator/Dockerfile
db-docker-compose.yaml
bot/utils/renderer/index.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@fairdataihub-bot
Copy link

Thanks for making updates to your pull request. Our team will take a look and provide feedback as soon as possible. Please wait for any GitHub Actions to complete before editing your pull request. If you have any additional questions or concerns, feel free to let us know. Thank you for your contributions!

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • On ui/pages/dashboard/[owner]/[repo]/edit/license.vue, the n-select for license selection was changed from v-model:value="licenseId" to a one-way binding :value="licenseId" without an @update:value handler, which means user selections will no longer update licenseId or trigger updateLicenseContent; consider restoring v-model:value or wiring @update:value="updateLicenseContent" (and updating licenseId there).
  • The new data-driven command routing in handlers/issue.js returns after the first matched trigger, whereas the previous implementation could execute multiple commands if multiple markers were present in the issue body; if workflows rely on multiple actions being triggered by a single issue edit, you may want to preserve the old behavior by removing the early return and allowing the loop to continue.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- On `ui/pages/dashboard/[owner]/[repo]/edit/license.vue`, the `n-select` for license selection was changed from `v-model:value="licenseId"` to a one-way binding `:value="licenseId"` without an `@update:value` handler, which means user selections will no longer update `licenseId` or trigger `updateLicenseContent`; consider restoring `v-model:value` or wiring `@update:value="updateLicenseContent"` (and updating `licenseId` there).
- The new data-driven command routing in `handlers/issue.js` returns after the first matched trigger, whereas the previous implementation could execute multiple commands if multiple markers were present in the issue body; if workflows rely on multiple actions being triggered by a single issue edit, you may want to preserve the old behavior by removing the early `return` and allowing the loop to continue.

## Individual Comments

### Comment 1
<location path="bot/index.js" line_range="46-50" />
<code_context>
-      });
-    }
-  });
+  // Register all event handlers
+  registerInstallationHandlers(app, db);
+  registerPushHandler(app, db);
+  registerPullRequestHandlers(app, db);
+  registerIssueHandlers(app, db);
 };
</code_context>
<issue_to_address>
**issue (bug_risk):** The `db` variable passed into handler registration is undefined; likely should be `dbInstance`.

`dbInstance` is imported in this module, but the handler registrations call `register*Handlers(app, db)` where `db` is not defined in scope. This will cause a `ReferenceError` on startup. These calls should use `dbInstance` instead of `db`.
</issue_to_address>

### Comment 2
<location path="bot/handlers/issue.js" line_range="123-87" />
<code_context>
+      }
+    }
+
+    // Data-driven command routing with early return
+    for (const { trigger, handler } of COMMANDS) {
+      if (issueBody.includes(trigger)) {
+        await handler(context, owner, repository, issueBody);
+        return;
+      }
+    }
</code_context>
<issue_to_address>
**question (bug_risk):** Early return on the first matching trigger changes behavior from running multiple commands per edit to only one.

The previous handler ran all matching commands because each `if` was independent. With the loop returning after the first match, only one command runs even if the body contains multiple triggers. If batching multiple commands in a single edit is expected, consider dropping the `return` and letting the loop continue, or collecting and running all matching handlers in sequence.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +46 to +50
// Register all event handlers
registerInstallationHandlers(app, db);
registerPushHandler(app, db);
registerPullRequestHandlers(app, db);
registerIssueHandlers(app, db);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): The db variable passed into handler registration is undefined; likely should be dbInstance.

dbInstance is imported in this module, but the handler registrations call register*Handlers(app, db) where db is not defined in scope. This will cause a ReferenceError on startup. These calls should use dbInstance instead of db.

logwatch.info(
"issues.edited: Issue title is not FAIR Compliance Dashboard or the editor is not the bot, ignoring..."
);
return;
Copy link

Choose a reason for hiding this comment

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

question (bug_risk): Early return on the first matching trigger changes behavior from running multiple commands per edit to only one.

The previous handler ran all matching commands because each if was independent. With the loop returning after the first match, only one command runs even if the body contains multiple triggers. If batching multiple commands in a single edit is expected, consider dropping the return and letting the loop continue, or collecting and running all matching handlers in sequence.

@slugb0t slugb0t merged commit 36e736c into main Feb 23, 2026
5 checks passed
@fairdataihub-bot
Copy link

Thanks for closing this pull request! If you have any further questions, please feel free to open a new issue. We are always happy to help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant