Skip to content

Conversation

@igorpecovnik
Copy link
Member

@igorpecovnik igorpecovnik commented Feb 1, 2026

Overview

Implements a comprehensive featured content generation system that automatically aggregates entries from multiple sources and randomly selects diverse content for the Armbian website.

Content Sources

  • GitHub Releases: Latest 5 releases from armbian/build
  • Ghost News: Latest 5 blog posts from armbian.com
  • GitHub Sponsors: Up to 10 active sponsors with tier information
  • Software List: Parsed from config.software.json nested menu structure
  • Manual Entries: 6 curated Armbian service promotions

Key Features

  • Per-category random selection: Selects N items from each content type
  • Type diversity: Ensures representation from all categories
  • Randomized output: Final selection shuffled for variety
  • Resilient error handling: Each source fails gracefully
  • Debug mode: Comprehensive logging for troubleshooting

Scripts

Script Purpose
github_releases.py Fetch latest releases
ghost_news.py Fetch blog posts from Ghost CMS
sponsors.py Fetch GitHub Sponsors with tier info
software_list.py Parse nested menu config structure
manual_list.py Load manually curated YAML entries
select_featured.py Random selection with diversity
ai_helper.py Optional AI content rewriting
orchestrator.py Legacy orchestration

Workflow

.github/workflows/featured-software.yml

  • Runs weekly via GitHub Actions
  • Copies config.software.json from configng repo
  • Executes each fetcher script separately
  • Selects 2 items per category randomly
  • Outputs featured-content.json at repo root

Technical Details

Software List Parsing

  • Recursively extracts items from nested menu structure
  • Filters to xxx001 items (install actions only)
  • Skips remove/purge/uninstall/disable keywords
  • Category-based tagging

Manual Featured Content

Promotes Armbian infrastructure:

  • Armbian Actions (CI/CD)
  • Mirror Network
  • Documentation
  • Community Forum
  • GitHub (open dev)
  • Sponsorship program

igorpecovnik and others added 2 commits February 1, 2026 23:52
Implements a comprehensive featured content system that aggregates entries
from multiple sources and randomly selects diverse content for display.

Content Sources:
- GitHub Releases: Latest Armbian build releases (5)
- Ghost News: Blog posts from armbian.com (5)
- GitHub Sponsors: Active sponsors with tier levels (10)
- Software List: From config.software.json, parsed from nested menu structure
- Manual Entries: Curated Armbian service promotions (6)

Key Features:
- Per-category random selection (N items from each type)
- Shuffle final output for variety
- Resilient error handling (graceful fallbacks)
- Debug mode for troubleshooting

Scripts:
- github_releases.py: Fetch latest releases from armbian/build
- ghost_news.py: Fetch blog posts from Ghost CMS
- sponsors.py: Fetch GitHub Sponsors with tier information
- software_list.py: Parse config.software.json nested menu structure
- manual_list.py: Load manually curated YAML entries
- select_featured.py: Random selection with type diversity
- ai_helper.py: Optional AI-powered content rewriting
- orchestrator.py: Legacy orchestration (not used in workflow)

Workflow (.github/workflows/featured-software.yml):
- Runs weekly via GitHub Actions
- Copies config.software.json from configng repository
- Executes each fetcher script separately with debug output
- Selects 2 items per category randomly
- Outputs featured-content.json at repo root

Software List Parsing:
- Recursively extracts items from nested menu structure
- Filters to xxx001 items (install actions only)
- Skips remove/purge/uninstall/disable actions
- Intelligent category-based tagging
- Random selection with type diversity

Manual Featured Content:
Promotes Armbian's own services and infrastructure:
- Armbian Actions (CI/CD builds)
- Mirror Network (global infrastructure)
- Documentation (comprehensive guides)
- Community Forum (support)
- GitHub (open development)
- Sponsorship program

Signed-off-by: Igor Pecovnik <igor@armbian.com>
@github-actions github-actions bot added 02 Milestone: First quarter release size/large PR with 250 lines or more GitHub Actions GitHub Actions code GitHub GitHub-related changes like labels, templates, ... Needs review Seeking for review labels Feb 1, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 1, 2026

Walkthrough

This pull request introduces a comprehensive featured content generation system. It adds a GitHub Actions workflow that periodically aggregates content from multiple sources—GitHub releases, Ghost CMS news, GitHub Sponsors, a software configuration file, and manual entries from YAML. The workflow orchestrates several Python modules that fetch, normalize, and process this content, selects a diverse subset of entries, and commits the result to featured-content.json. The system includes AI-powered summary rewriting via Google Gemini, error handling with fallbacks to empty arrays, and conditional execution based on available API credentials.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add automated featured content generation system' accurately summarizes the main change—a new system for automatically generating featured content from multiple sources.
Docstring Coverage ✅ Passed Docstring coverage is 92.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The pull request description is clearly related to the changeset, providing a comprehensive overview of the automated featured content generation system including content sources, key features, scripts, workflow details, and technical implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch featured

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@igorpecovnik igorpecovnik added Work in progress Unfinished / work in progress and removed Needs review Seeking for review labels Feb 1, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Fix all issues with AI agents
In @.github/workflows/featured-software.yml:
- Around line 6-10: The workflow exposes a sensitive API key via the
workflow_dispatch input named gemini_api_key; remove the gemini_api_key input
from the inputs block and update any steps that reference it to instead read
from the repository secret secrets.GEMINI_API_KEY (e.g., use env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} or reference it directly in the
step), ensuring no plain-text inputs remain in the featured-software.yml
workflow.

In `@featured-content.json`:
- Around line 51-55: The featured-content JSON contains HTTP URLs for
blog.armbian.com in the "url" and "image" fields; update these entries to use
HTTPS (e.g., change "http://blog.armbian.com/..." to
"https://blog.armbian.com/...") and also change the other occurrences noted;
additionally, update the Ghost fetching/normalization code in ghost_news.py
(e.g., the function that builds or normalizes URLs—rename/locate code like
normalize_url or the URL construction block) to enforce scheme="https" for
blog.armbian.com by parsing the URL, setting url.scheme = "https" (or replacing
the prefix) before saving so future fetched entries are normalized to HTTPS.

In `@featured-content/scripts/.gitignore`:
- Line 1: The .gitignore entry is using the full path
'featured-content/scripts/__pycache__/' which is redundant because the
.gitignore lives in that directory; update the pattern to '__pycache__/' so it
correctly ignores the local __pycache__ directory (replace the existing
'featured-content/scripts/__pycache__/' entry with '__pycache__/').

In `@featured-content/scripts/ghost_news.py`:
- Around line 37-46: The urlopen call in the fetch logic currently has no
timeout; update the urllib.request.urlopen(req) call to include a timeout (e.g.,
urllib.request.urlopen(req, timeout=10)) or use the project-wide HTTP timeout
constant if one exists, so requests won’t hang indefinitely. Locate the block
that builds req and calls urllib.request.urlopen (the variables req, headers and
the except Exception as e handler) and pass a timeout parameter to urlopen;
ensure the timeout value is consistent with other scripts in the repo and keep
the existing error handling intact.

In `@featured-content/scripts/github_releases.py`:
- Around line 28-35: The urlopen call uses no timeout and can hang; define a
sensible constant (e.g. TIMEOUT_SECONDS = 10) near the top of the module and
change the call to urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) (or
urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) if you wrap a Request),
and update the existing except block around urlopen to also handle
timeout/URLError cases (e.g. socket.timeout / urllib.error.URLError) so the
function returns [] on timeout instead of hanging.
- Around line 38-49: process_releases currently ignores the use_ai flag and
always uses raw release body text; update it to mirror ghost_news.py and
sponsors.py by conditionally calling rewrite_summary_with_ai(release_body) when
use_ai is True (and falling back to raw release_body when False), or remove the
use_ai parameter if AI rewriting is not desired; specifically modify
process_releases to check use_ai and invoke rewrite_summary_with_ai (same
function used by other modules) for each release summary before building the
contribution entry.

In `@featured-content/scripts/manual_list.py`:
- Around line 74-116: In process_manual_entries, the use_ai parameter is ignored
because rewrite_summary_with_ai is always called; update the logic in
process_manual_entries to only call rewrite_summary_with_ai(title, summary,
name, entry_type) when use_ai is truthy and otherwise leave summary unchanged
(or skip the call), and ensure summary retains its original value when use_ai is
False; reference the process_manual_entries function and the
rewrite_summary_with_ai import to locate the change.

In `@featured-content/scripts/orchestrator.py`:
- Around line 100-115: The function erroneously re-imports os locally which
creates a local binding and causes UnboundLocalError when os.path.exists() is
called earlier; remove the redundant "import os" near the manual_path
construction and rely on the module-level import so manual_path =
os.path.join(os.path.dirname(__file__), '..', 'manual_featured.yml') uses the
global os; update or run tests to verify no reference-before-assignment remains.
- Around line 260-263: The empty-result branch currently prints a bare "[]"
which is inconsistent with the successful path that emits an object with
"entries", "count", and "sources"; update the branch that checks all_entries to
print the same object shape (e.g. {"entries": [], "count": 0, "sources": {}})
instead of "[]", keeping the stderr debug message if desired. Locate the
conditional that references all_entries and the two print calls and replace the
stdout print to emit the JSON object with entries as an empty list, count zero,
and an empty sources map so downstream consumers always receive the same schema.

In `@featured-content/scripts/software_list.py`:
- Around line 102-115: The function process_software_entries currently ignores
the use_ai flag and always calls rewrite_summary_with_ai; update the logic
inside process_software_entries to conditionally call rewrite_summary_with_ai
only when use_ai is truthy and otherwise leave the original summary (or a non-AI
fallback) intact. Locate the call to rewrite_summary_with_ai (and related
summary assignment) and wrap it behind an if use_ai: ... else: ... so other
modules can disable AI rewriting by passing use_ai=False.

In `@featured-content/scripts/sponsors.py`:
- Around line 84-91: The urllib.request.urlopen call currently has no timeout
and can hang indefinitely; update the code that builds and sends the request
(the urllib.request.Request construction and the urllib.request.urlopen call) to
pass a sensible timeout (e.g., timeout=10) to urlopen (or pull a TIMEOUT_SECS
constant and use that) so the HTTP call fails fast on network issues; ensure the
change is applied to the code path that uses url, headers, and query so response
= urllib.request.urlopen(req, timeout=...) is used instead of the current
urlopen(req).
- Around line 14-34: The fetch_sponsors function currently takes a repo
parameter but never uses it (the GraphQL query is hardcoded to "armbian"), and
it prints a token preview to stderr; fix by either removing the unused repo
argument from fetch_sponsors (and any callers) or modifying the GraphQL
construction to parse repo into organization/repo and use that value, and stop
printing the token preview: replace the token_preview print with a non-sensitive
presence check (e.g., log only that GITHUB_TOKEN is set and its length) or make
printing conditional on a debug flag (check an env var like DEBUG/VERBOSE),
referencing the fetch_sponsors function, the api_key variable, and token_preview
symbol to locate the changes.
🧹 Nitpick comments (14)
featured-content.json (1)

67-84: Empty URL fields in software entries.

The software entries at Lines 70 and 177 have empty url fields. If URLs are not available for all software items, consider either omitting the field entirely or providing a fallback URL (e.g., to the armbian-config documentation).

featured-content/scripts/select_featured.py (1)

14-20: Add validation for per_category_count argument.

If sys.argv[1] is not a valid integer, int() will raise a ValueError with an unhelpful message. Consider wrapping with a try-except or using argparse for consistent CLI handling with other scripts in this PR.

🛠️ Proposed fix using argparse for consistency
+import argparse
+
+
 def main():
-    if len(sys.argv) < 3:
-        print("Usage: select_featured.py <per_category_count> <input1.json> [input2.json] ...")
-        sys.exit(1)
-
-    per_category_count = int(sys.argv[1])
-    input_files = sys.argv[2:]
+    parser = argparse.ArgumentParser(description='Select diverse featured entries')
+    parser.add_argument('per_category_count', type=int, help='Number of entries per category')
+    parser.add_argument('input_files', nargs='+', help='Input JSON files')
+    args = parser.parse_args()
+
+    per_category_count = args.per_category_count
+    input_files = args.input_files
featured-content/scripts/github_releases.py (1)

78-92: Summary truncation may cut words mid-sentence.

Line 79 truncates the body at exactly 200 characters, which could cut a word in half. Consider truncating at a word boundary.

♻️ Proposed improvement
-        summary = body[:200].strip() + '...' if len(body) > 200 else body.strip()
+        if len(body) > 200:
+            # Truncate at word boundary
+            summary = body[:200].rsplit(' ', 1)[0].strip() + '...'
+        else:
+            summary = body.strip()
featured-content/scripts/ai_helper.py (2)

25-29: Remove extraneous f prefix from string literals.

Lines 28, 70, and 83 use f-strings without any placeholders. These should be regular strings.

♻️ Proposed fix
-        print(f"  No API key found, keeping original summary", file=sys.stderr)
+        print("  No API key found, keeping original summary", file=sys.stderr)
-            print(f"  AI returned empty response, using original summary", file=sys.stderr)
+            print("  AI returned empty response, using original summary", file=sys.stderr)
-            print(f"  AI returned too short result, using original summary", file=sys.stderr)
+            print("  AI returned too short result, using original summary", file=sys.stderr)

59-66: Consider lowering temperature for more consistent summaries.

A temperature of 0.9 is quite high and may produce inconsistent or overly creative results across runs. For summary rewriting where consistency is desirable, consider a temperature between 0.3-0.7.

featured-content/scripts/sponsors.py (2)

100-107: Remove extraneous f prefix and clean up unused variable.

Line 101 has an f-string without placeholders. Also, Line 179 assigns location but never uses it.

♻️ Proposed fixes
-                print(f"Debug: GraphQL errors:", file=sys.stderr)
+                print("Debug: GraphQL errors:", file=sys.stderr)
-        location = sponsor.get('location', '')
+        # location = sponsor.get('location', '')  # Unused, remove if not needed

172-189: Remove extraneous f prefix in default description.

Line 175 uses an f-string without placeholders for the default description.

♻️ Proposed fix
-        description = sponsor.get('description', f"Sponsor of Armbian build")[:200]
+        description = sponsor.get('description', "Sponsor of Armbian build")[:200]
featured-content/scripts/software_list.py (2)

102-115: Unused featured_only parameter.

The featured_only parameter is declared but never used in the function body. The filtering logic at Lines 130-142 always runs. Either implement the conditional logic or remove the parameter.

♻️ Proposed fix to implement the parameter
     # Filter to items in featured categories or with high-quality metadata
     featured_items = []
-    for item in all_software:
+    items_to_filter = all_software
+    
+    if not featured_only:
+        # Skip filtering, use all items
+        featured_items = all_software
+    else:
+      for item in items_to_filter:
         parent = item.get('parent_menu', '')

145-152: Move import random to module level.

While importing inside the function works, it's more idiomatic to place imports at the module level. This also avoids repeated import overhead if the function is called multiple times.

featured-content/scripts/ghost_news.py (1)

89-91: Update docstring to document "news" entry type support.

The rewrite_summary_with_ai function in ai_helper.py accepts "news" as an entry type (line 89 in ghost_news.py), but the docstring (line 20) only documents "software" and "contribution". The "news" type falls through to the else branch, using the generic prompt intended for software. Either update the docstring to list all accepted entry types or add explicit handling with a news-specific prompt.

.github/workflows/featured-software.yml (3)

3-5: Push trigger is overly broad and will run on every commit to any branch.

The on: push without path or branch filters means this workflow will execute on every push to every branch, consuming CI resources unnecessarily.

Consider restricting to the main branch and relevant paths:

♻️ Suggested filter
 on:
   push:
+    branches: [main]
+    paths:
+      - 'featured-content/**'
+      - '.github/workflows/featured-software.yml'
   workflow_dispatch:

113-127: Consider explicit branch reference for git push.

If this workflow runs on a non-default branch (given the current broad push trigger), git push without a branch reference may behave unexpectedly.

♻️ Suggested improvement
-            git push
+            git push origin HEAD

45-47: Remove --upgrade flag and pin to tested versions for reproducibility.

Unpinned dependencies with --upgrade can cause unexpected breakages when new versions are released. Instead of upgrading to the latest, pin to specific versions that have been tested with this workflow. Current available versions are google-genai==1.58.0 and pyyaml==6.0.3, though you should test and pin to whichever versions work best for your use case rather than always pulling the latest.

♻️ Suggested improvement
       - name: Install Python dependencies
         run: |
-          pip3 install --upgrade google-genai pyyaml
+          pip3 install google-genai==1.58.0 pyyaml==6.0.3
featured-content/scripts/manual_list.py (1)

49-51: Remove extraneous f prefix from string literal.

This f-string contains no placeholders.

♻️ Proposed fix
     if not HAS_YAML:
-        print(f"Warning: PyYAML not installed, run 'pip install pyyaml'", file=sys.stderr)
+        print("Warning: PyYAML not installed, run 'pip install pyyaml'", file=sys.stderr)
         return {"entries": []}

Comment on lines +6 to +10
inputs:
gemini_api_key:
description: 'Google Gemini API Key for AI-powered summary generation'
required: false
type: string
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: API key passed via workflow_dispatch input is not masked.

Workflow dispatch inputs of type string are logged in plain text in workflow run logs. If someone triggers this workflow manually with an API key, it will be visible in the logs.

Consider removing this input and relying solely on secrets.GEMINI_API_KEY:

🔒 Suggested fix
   workflow_dispatch:
-    inputs:
-      gemini_api_key:
-        description: 'Google Gemini API Key for AI-powered summary generation'
-        required: false
-        type: string
   schedule:
 env:
-  GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY || inputs.gemini_api_key }}
+  GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
🤖 Prompt for AI Agents
In @.github/workflows/featured-software.yml around lines 6 - 10, The workflow
exposes a sensitive API key via the workflow_dispatch input named
gemini_api_key; remove the gemini_api_key input from the inputs block and update
any steps that reference it to instead read from the repository secret
secrets.GEMINI_API_KEY (e.g., use env: GEMINI_API_KEY: ${{
secrets.GEMINI_API_KEY }} or reference it directly in the step), ensuring no
plain-text inputs remain in the featured-software.yml workflow.

Comment on lines +51 to +55
"url": "http://blog.armbian.com/forget-third-party-utilities-meet-armbian-imager/",
"title": "Forget third-party utilities: meet Armbian Imager",
"summary": "Armbian Imager eliminates the guesswork from flashing SBC images. Real-time board detection, persistent caching, and built-in safety make installation fast, simple, and risk-free",
"published_at": "2026-01-17T14:43:51.000+01:00",
"image": "http://blog.armbian.com/content/images/2026/01/introducing-armbian-imager.png",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

HTTP URLs should use HTTPS for security.

The Ghost news entries contain HTTP URLs (Lines 52, 55) instead of HTTPS. Since blog.armbian.com likely supports HTTPS, the fetching script (ghost_news.py) should ensure URLs are normalized to HTTPS to prevent mixed-content warnings and improve security.

Similarly, Lines 119 and 123 have the same issue.

🤖 Prompt for AI Agents
In `@featured-content.json` around lines 51 - 55, The featured-content JSON
contains HTTP URLs for blog.armbian.com in the "url" and "image" fields; update
these entries to use HTTPS (e.g., change "http://blog.armbian.com/..." to
"https://blog.armbian.com/...") and also change the other occurrences noted;
additionally, update the Ghost fetching/normalization code in ghost_news.py
(e.g., the function that builds or normalizes URLs—rename/locate code like
normalize_url or the URL construction block) to enforce scheme="https" for
blog.armbian.com by parsing the URL, setting url.scheme = "https" (or replacing
the prefix) before saving so future fetched entries are normalized to HTTPS.

@@ -0,0 +1,2 @@
featured-content/scripts/__pycache__/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the pycache ignore pattern.

The path featured-content/scripts/__pycache__/ is relative to this .gitignore file's location. Since this file is already in featured-content/scripts/, the pattern will look for a nested path that doesn't exist. Use __pycache__/ instead to correctly ignore the directory in the current location.

📁 Proposed fix
-featured-content/scripts/__pycache__/
+__pycache__/
 *.pyc
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
featured-content/scripts/__pycache__/
__pycache__/
*.pyc
🤖 Prompt for AI Agents
In `@featured-content/scripts/.gitignore` at line 1, The .gitignore entry is using
the full path 'featured-content/scripts/__pycache__/' which is redundant because
the .gitignore lives in that directory; update the pattern to '__pycache__/' so
it correctly ignores the local __pycache__ directory (replace the existing
'featured-content/scripts/__pycache__/' entry with '__pycache__/').

Comment on lines +37 to +46
try:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
posts = data.get('posts', [])
print(f"Fetched {len(posts)} posts from Ghost", file=sys.stderr)
return posts
except Exception as e:
print(f"Error fetching Ghost posts: {e}", file=sys.stderr)
return []
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add timeout to HTTP request.

The urlopen call at Line 39 lacks a timeout, consistent with the pattern in other scripts. All HTTP requests should have timeouts for reliability.

🛠️ Proposed fix
     try:
         req = urllib.request.Request(url, headers=headers)
-        with urllib.request.urlopen(req) as response:
+        with urllib.request.urlopen(req, timeout=30) as response:
🧰 Tools
🪛 Ruff (0.14.14)

[error] 38-38: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 39-39: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[warning] 44-44: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@featured-content/scripts/ghost_news.py` around lines 37 - 46, The urlopen
call in the fetch logic currently has no timeout; update the
urllib.request.urlopen(req) call to include a timeout (e.g.,
urllib.request.urlopen(req, timeout=10)) or use the project-wide HTTP timeout
constant if one exists, so requests won’t hang indefinitely. Locate the block
that builds req and calls urllib.request.urlopen (the variables req, headers and
the except Exception as e handler) and pass a timeout parameter to urlopen;
ensure the timeout value is consistent with other scripts in the repo and keep
the existing error handling intact.

Comment on lines +28 to +35
try:
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
print(f"Fetched {len(data)} releases from GitHub", file=sys.stderr)
return data
except Exception as e:
print(f"Error fetching releases: {e}", file=sys.stderr)
return []
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add timeout to HTTP request to prevent indefinite hangs.

The urlopen call at Line 29 has no timeout, which could cause the workflow to hang indefinitely if the GitHub API is unresponsive.

🛠️ Proposed fix
     try:
-        with urllib.request.urlopen(url) as response:
+        with urllib.request.urlopen(url, timeout=30) as response:
             data = json.loads(response.read().decode())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
print(f"Fetched {len(data)} releases from GitHub", file=sys.stderr)
return data
except Exception as e:
print(f"Error fetching releases: {e}", file=sys.stderr)
return []
try:
with urllib.request.urlopen(url, timeout=30) as response:
data = json.loads(response.read().decode())
print(f"Fetched {len(data)} releases from GitHub", file=sys.stderr)
return data
except Exception as e:
print(f"Error fetching releases: {e}", file=sys.stderr)
return []
🧰 Tools
🪛 Ruff (0.14.14)

[error] 29-29: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[warning] 33-33: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@featured-content/scripts/github_releases.py` around lines 28 - 35, The
urlopen call uses no timeout and can hang; define a sensible constant (e.g.
TIMEOUT_SECONDS = 10) near the top of the module and change the call to
urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) (or
urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) if you wrap a Request),
and update the existing except block around urlopen to also handle
timeout/URLError cases (e.g. socket.timeout / urllib.error.URLError) so the
function returns [] on timeout instead of hanging.

Comment on lines +100 to +115
if config_file and os.path.exists(config_file):
print(f"Loading software config from {config_file}...", file=sys.stderr)
config = software_list.load_software_config(config_file)
if config:
software_entries = software_list.process_software_entries(
config, limit=software_limit, featured_only=True, use_ai=use_ai
)
all_entries.extend(software_entries)
counts["software"] = len(software_entries)
print(f" Added {len(software_entries)} software entries", file=sys.stderr)

# 6. Manual curated entries
print("Loading manual featured entries...", file=sys.stderr)
# Look in parent directory (featured-content/) for manual_featured.yml
import os
manual_path = os.path.join(os.path.dirname(__file__), '..', 'manual_featured.yml')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Redundant import os shadows module-level import and causes reference-before-assignment.

os is already imported at the module level (Line 13), but there's a redundant import os at Line 114. The issue is that os.path.exists() is called at Line 100, before the local import at Line 114 executes in the function scope.

Python's scoping rules mean the local import os at Line 114 makes the entire function treat os as a local variable, so Line 100 fails with UnboundLocalError.

🐛 Proposed fix - remove redundant import
     # 6. Manual curated entries
     print("Loading manual featured entries...", file=sys.stderr)
     # Look in parent directory (featured-content/) for manual_featured.yml
-    import os
     manual_path = os.path.join(os.path.dirname(__file__), '..', 'manual_featured.yml')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if config_file and os.path.exists(config_file):
print(f"Loading software config from {config_file}...", file=sys.stderr)
config = software_list.load_software_config(config_file)
if config:
software_entries = software_list.process_software_entries(
config, limit=software_limit, featured_only=True, use_ai=use_ai
)
all_entries.extend(software_entries)
counts["software"] = len(software_entries)
print(f" Added {len(software_entries)} software entries", file=sys.stderr)
# 6. Manual curated entries
print("Loading manual featured entries...", file=sys.stderr)
# Look in parent directory (featured-content/) for manual_featured.yml
import os
manual_path = os.path.join(os.path.dirname(__file__), '..', 'manual_featured.yml')
if config_file and os.path.exists(config_file):
print(f"Loading software config from {config_file}...", file=sys.stderr)
config = software_list.load_software_config(config_file)
if config:
software_entries = software_list.process_software_entries(
config, limit=software_limit, featured_only=True, use_ai=use_ai
)
all_entries.extend(software_entries)
counts["software"] = len(software_entries)
print(f" Added {len(software_entries)} software entries", file=sys.stderr)
# 6. Manual curated entries
print("Loading manual featured entries...", file=sys.stderr)
# Look in parent directory (featured-content/) for manual_featured.yml
manual_path = os.path.join(os.path.dirname(__file__), '..', 'manual_featured.yml')
🧰 Tools
🪛 Ruff (0.14.14)

[error] 100-100: Local variable os referenced before assignment

(F823)

🤖 Prompt for AI Agents
In `@featured-content/scripts/orchestrator.py` around lines 100 - 115, The
function erroneously re-imports os locally which creates a local binding and
causes UnboundLocalError when os.path.exists() is called earlier; remove the
redundant "import os" near the manual_path construction and rely on the
module-level import so manual_path = os.path.join(os.path.dirname(__file__),
'..', 'manual_featured.yml') uses the global os; update or run tests to verify
no reference-before-assignment remains.

Comment on lines +260 to +263
if not all_entries:
print("No entries fetched, outputting empty array", file=sys.stderr)
print("[]")
return
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent output format when no entries are fetched.

The empty case outputs [] (bare array), while the success path outputs {"entries": [...], "count": N, "sources": {...}}. This inconsistency could break downstream consumers expecting the object format.

🐛 Proposed fix for consistent output
     if not all_entries:
         print("No entries fetched, outputting empty array", file=sys.stderr)
-        print("[]")
+        print(json.dumps({"entries": [], "count": 0, "sources": counts, "generated_at": None}, indent=2))
         return
🤖 Prompt for AI Agents
In `@featured-content/scripts/orchestrator.py` around lines 260 - 263, The
empty-result branch currently prints a bare "[]" which is inconsistent with the
successful path that emits an object with "entries", "count", and "sources";
update the branch that checks all_entries to print the same object shape (e.g.
{"entries": [], "count": 0, "sources": {}}) instead of "[]", keeping the stderr
debug message if desired. Locate the conditional that references all_entries and
the two print calls and replace the stdout print to emit the JSON object with
entries as an empty list, count zero, and an empty sources map so downstream
consumers always receive the same schema.

Comment on lines +102 to +115
def process_software_entries(config, limit=None, featured_only=True, use_ai=True):
"""
Process software entries from Armbian config into featured entries.

Args:
config: Loaded configuration dictionary
limit: Maximum number of entries to process (default: all)
featured_only: Only include entries marked as featured (not used for menu format)
use_ai: Whether to use AI to rewrite summaries

Returns:
List of software entry dictionaries
"""
from ai_helper import rewrite_summary_with_ai
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused use_ai parameter - AI rewriting always occurs.

The use_ai parameter is declared but never checked. Line 198 always calls rewrite_summary_with_ai. This should be conditional based on the parameter, consistent with other modules.

🛠️ Proposed fix
-        # Rewrite summary with AI
-        title = name
-        summary = rewrite_summary_with_ai(title, description, name, "software")
+        # Rewrite summary with AI if enabled
+        title = name
+        if use_ai:
+            summary = rewrite_summary_with_ai(title, description, name, "software")
+        else:
+            summary = description
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 102-102: Unused function argument: featured_only

(ARG001)


[warning] 102-102: Unused function argument: use_ai

(ARG001)

🤖 Prompt for AI Agents
In `@featured-content/scripts/software_list.py` around lines 102 - 115, The
function process_software_entries currently ignores the use_ai flag and always
calls rewrite_summary_with_ai; update the logic inside process_software_entries
to conditionally call rewrite_summary_with_ai only when use_ai is truthy and
otherwise leave the original summary (or a non-AI fallback) intact. Locate the
call to rewrite_summary_with_ai (and related summary assignment) and wrap it
behind an if use_ai: ... else: ... so other modules can disable AI rewriting by
passing use_ai=False.

Comment on lines +14 to +34
def fetch_sponsors(repo="armbian/build", limit=10):
"""
Fetch sponsors from GitHub Sponsors API.

Args:
repo: GitHub repository (default: armbian/build)
limit: Number of sponsors to fetch (default: 10)

Returns:
List of sponsor dictionaries
"""
# GitHub GraphQL API for sponsors
api_key = os.environ.get('GITHUB_TOKEN')
if not api_key:
print("Error: GITHUB_TOKEN environment variable not set", file=sys.stderr)
print("Hint: Ensure the workflow has: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", file=sys.stderr)
return []

# Debug: Show token presence (truncated for security)
token_preview = api_key[:8] + "..." if len(api_key) > 8 else "..."
print(f"Debug: GITHUB_TOKEN found: {token_preview} (len={len(api_key)})", file=sys.stderr)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused repo parameter and token logging concerns.

  1. The repo parameter (Line 14) is never used - the GraphQL query hardcodes "armbian". Either implement dynamic organization lookup or remove the parameter.

  2. Line 34 logs a preview of the token to stderr. While truncated, this could still appear in CI logs. Consider removing or making it conditional on a verbose/debug flag.

🛠️ Proposed fix for token logging
-    # Debug: Show token presence (truncated for security)
-    token_preview = api_key[:8] + "..." if len(api_key) > 8 else "..."
-    print(f"Debug: GITHUB_TOKEN found: {token_preview} (len={len(api_key)})", file=sys.stderr)
+    # Debug: Confirm token presence without exposing content
+    print(f"Debug: GITHUB_TOKEN found (len={len(api_key)})", file=sys.stderr)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def fetch_sponsors(repo="armbian/build", limit=10):
"""
Fetch sponsors from GitHub Sponsors API.
Args:
repo: GitHub repository (default: armbian/build)
limit: Number of sponsors to fetch (default: 10)
Returns:
List of sponsor dictionaries
"""
# GitHub GraphQL API for sponsors
api_key = os.environ.get('GITHUB_TOKEN')
if not api_key:
print("Error: GITHUB_TOKEN environment variable not set", file=sys.stderr)
print("Hint: Ensure the workflow has: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", file=sys.stderr)
return []
# Debug: Show token presence (truncated for security)
token_preview = api_key[:8] + "..." if len(api_key) > 8 else "..."
print(f"Debug: GITHUB_TOKEN found: {token_preview} (len={len(api_key)})", file=sys.stderr)
def fetch_sponsors(repo="armbian/build", limit=10):
"""
Fetch sponsors from GitHub Sponsors API.
Args:
repo: GitHub repository (default: armbian/build)
limit: Number of sponsors to fetch (default: 10)
Returns:
List of sponsor dictionaries
"""
# GitHub GraphQL API for sponsors
api_key = os.environ.get('GITHUB_TOKEN')
if not api_key:
print("Error: GITHUB_TOKEN environment variable not set", file=sys.stderr)
print("Hint: Ensure the workflow has: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", file=sys.stderr)
return []
# Debug: Confirm token presence without exposing content
print(f"Debug: GITHUB_TOKEN found (len={len(api_key)})", file=sys.stderr)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 14-14: Unused function argument: repo

(ARG001)

🤖 Prompt for AI Agents
In `@featured-content/scripts/sponsors.py` around lines 14 - 34, The
fetch_sponsors function currently takes a repo parameter but never uses it (the
GraphQL query is hardcoded to "armbian"), and it prints a token preview to
stderr; fix by either removing the unused repo argument from fetch_sponsors (and
any callers) or modifying the GraphQL construction to parse repo into
organization/repo and use that value, and stop printing the token preview:
replace the token_preview print with a non-sensitive presence check (e.g., log
only that GITHUB_TOKEN is set and its length) or make printing conditional on a
debug flag (check an env var like DEBUG/VERBOSE), referencing the fetch_sponsors
function, the api_key variable, and token_preview symbol to locate the changes.

Comment on lines +84 to +91
try:
print(f"Debug: Sending request to {url}", file=sys.stderr)
req = urllib.request.Request(url, headers=headers, data=json.dumps(query).encode())
with urllib.request.urlopen(req) as response:
status = response.status
body = response.read().decode()
print(f"Debug: Response status: {status}", file=sys.stderr)
print(f"Debug: Response length: {len(body)} bytes", file=sys.stderr)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add timeout to HTTP request.

The urlopen call at Line 87 has no timeout, which could cause indefinite hangs. This is a recurring pattern across the scripts in this PR.

🛠️ Proposed fix
-        with urllib.request.urlopen(req) as response:
+        with urllib.request.urlopen(req, timeout=30) as response:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
print(f"Debug: Sending request to {url}", file=sys.stderr)
req = urllib.request.Request(url, headers=headers, data=json.dumps(query).encode())
with urllib.request.urlopen(req) as response:
status = response.status
body = response.read().decode()
print(f"Debug: Response status: {status}", file=sys.stderr)
print(f"Debug: Response length: {len(body)} bytes", file=sys.stderr)
try:
print(f"Debug: Sending request to {url}", file=sys.stderr)
req = urllib.request.Request(url, headers=headers, data=json.dumps(query).encode())
with urllib.request.urlopen(req, timeout=30) as response:
status = response.status
body = response.read().decode()
print(f"Debug: Response status: {status}", file=sys.stderr)
print(f"Debug: Response length: {len(body)} bytes", file=sys.stderr)
🧰 Tools
🪛 Ruff (0.14.14)

[error] 86-86: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 87-87: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
In `@featured-content/scripts/sponsors.py` around lines 84 - 91, The
urllib.request.urlopen call currently has no timeout and can hang indefinitely;
update the code that builds and sends the request (the urllib.request.Request
construction and the urllib.request.urlopen call) to pass a sensible timeout
(e.g., timeout=10) to urlopen (or pull a TIMEOUT_SECS constant and use that) so
the HTTP call fails fast on network issues; ensure the change is applied to the
code path that uses url, headers, and query so response =
urllib.request.urlopen(req, timeout=...) is used instead of the current
urlopen(req).

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

Labels

02 Milestone: First quarter release GitHub Actions GitHub Actions code GitHub GitHub-related changes like labels, templates, ... size/large PR with 250 lines or more Work in progress Unfinished / work in progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant