Skip to content

action: emit assets manifest JSON alongside release uploads#9772

Merged
igorpecovnik merged 3 commits into
mainfrom
action-release-assets-manifest
May 6, 2026
Merged

action: emit assets manifest JSON alongside release uploads#9772
igorpecovnik merged 3 commits into
mainfrom
action-release-assets-manifest

Conversation

@igorpecovnik
Copy link
Copy Markdown
Member

@igorpecovnik igorpecovnik commented May 6, 2026

Summary

Expands the composite action so each build run emits a small JSON manifest describing every image it produced, written next to the image artefacts and uploaded to the GitHub release alongside them. The schema mirrors the canonical https://github.armbian.com/armbian-images.json 1:1, so a downstream aggregator can flatten per-build fragments into the central index with jq -s 'map(.assets) | flatten' and no bespoke parsing.

Use case: armbian/sdk and any other consumer of armbian/build@main ends up with a per-image manifest in its release. Easy to consume.

What the action does

After the build (and Sign) step, before ncipollo/release-action:

  1. Walk build/output/images for each known image format — img, img.qcow2, img.vhdx, hyperv.zip — in their xz / zst / raw forms.
  2. Group by (stem, image_format). Within each group keep the most-compressed form. Different formats coexist as separate manifest entries — the raw image and the qcow2 build of the same logical image are distinct products.
  3. Parse each filename via a tolerant regex that accepts Armbian_… and Armbian-unofficial_… prefixes and treats the trailing _variant[.app] segment as optional.
  4. Read board metadata (BOARD_NAME, BOARD_VENDOR, BOARD_SUPPORT_LEVEL, INTRODUCED) from the build framework's existing build/output/info/image-info.json.
  5. Enrich by fetching the canonical published index. Populates company_name / company_website / company_logo (Bigin-resolved by armbian.github.io's pipeline) and platinum / platinum_expired / platinum_until, plus a fallback for board_* fields when image-info.json doesn't have them.
  6. Compose file_url and redi_url (and their .asc / .sha / .torrent siblings) from a configurable base URL + repository segment.
  7. Write <image_filename>.assets.json next to each image. The existing ncipollo/release-action glob (${{ inputs.armbian_artifacts }}*) picks them up automatically — no caller change needed.

Per-image filenames are unique by construction across both multiple formats in one build call AND parallel matrix cells with different extension lists, so per-cell-named manifests don't collide on the release.

New optional inputs

Input Default Purpose
armbian_download_base_url https://dl.armbian.com Base URL where published images live (used to construct file_url / redi_url).
armbian_download_repository archive Repository segment under <base>/<board>/<repo>/. Set to empty string for the flat URL shape <base>/<filename> — useful when images publish directly to a GitHub release. Leading/trailing slashes are stripped.
armbian_index_url https://github.armbian.com/armbian-images.json Canonical index used for company_* / platinum_* enrichment. Empty disables.

URL shapes

armbian_download_repository selects between two URL conventions:

  • set (default archive){base}/{board}/{repo}/{filename}. redi_url* uses the friendly {Distro}_{branch}_{variant}[-format|app] redirect path. Format suffix wins over app suffix (canonical convention: minimal-qcow2, minimal-hyperv).
  • empty{base}/{filename} (flat). redi_url* mirror file_url* since GitHub releases have no friendly-redirect convention.

Schema

{
  "assets": [
    {
      "board_slug":          "uefi-arm64",
      "board_name":          "UEFI arm64",
      "board_vendor":        "generic",
      "board_support":       "conf",
      "board_introduced":    "2022",
      "company_name":        "Generic",
      "company_website":     "",
      "company_logo":        "https://cache.armbian.com/images/vendors/150/generic.png",
      "armbian_version":     "26.05.0-trunk",
      "file_url":            "https://dl.armbian.com/uefi-arm64/archive/Armbian_…_minimal.img.xz",
      "file_url_asc":        "…/…img.xz.asc",
      "file_url_sha":        "…/…img.xz.sha",
      "file_url_torrent":    "…/…img.xz.torrent",
      "redi_url":            "https://dl.armbian.com/uefi-arm64/Noble_current_minimal",
      "redi_url_asc":        "",
      "redi_url_sha":        "",
      "redi_url_torrent":    "",
      "file_size":           "299047896",
      "file_date":           "2026-05-06T08:11:42Z",
      "distro":              "noble",
      "branch":              "current",
      "variant":             "minimal",
      "file_application":    "",
      "promoted":            "false",
      "download_repository": "archive",
      "file_extension":      "img.xz",
      "kernel_version":      "6.18.21",
      "platinum":            "false",
      "platinum_expired":    "false",
      "platinum_until":      ""
    }
  ]
}

Filename parsing

Mirrors generate-armbian-images-json.sh:

  • Variant token: _variant[.app] (e.g. minimal.hyperv). The .app part is optional.
  • Kernel suffix: rsplit at the LAST -. The trailing token becomes file_application iff it's all letters (so sdk, omv, kali, homeassistant, openhab, ufs → app; rc5, beta1, alpha2, pre3 stay attached to kernel_version). This way 6.6.0-rc1-sdk parses as kernel_version=6.6.0-rc1, file_application=sdk.
  • Variant-side app wins when both the variant suffix and the kernel suffix carry an app.

Test results

Dry-run against an 18-image build/output/images/ tree on a developer host:

  • 18/18 filenames parse (regex tolerates the variant-less form, the Armbian-unofficial prefix, etc.).
  • board_name / board_vendor / board_support / board_introduced populate from image-info.json for boards listed there; fall back to the canonical index for others.
  • company_* and platinum_* populate from the live published index (340 boards loaded; uefi-arm64 resolves to company_name="Generic" / company_logo=…/generic.png; inovato-quadra resolves to platinum="true" / platinum_until="2026-06-15").
  • Synthetic SDK-style filenames (6.18.26-sdk) strip cleanly to kernel_version="6.18.26" / file_application="sdk". Multi-dash cases (6.6.0-rc1-sdk) preserve the rc tag. Edge cases (rc5/beta1/alpha2/pre3 without app, hyperv variant suffixes) all behave correctly.
  • All ::warning:: workflow commands go to stdout so GitHub Actions parses them into proper annotations.

Test plan

  • Trigger an armbian/sdk workflow (which uses this action). Confirm the resulting GitHub release contains an <image_filename>.assets.json per .img* artefact.
  • jq '.assets | length' *.assets.json returns 1 for each.
  • Spot-check one entry: file_url resolves to a downloadable file, redi_url matches the existing dl.armbian.com redirect convention (or mirrors file_url in flat mode), and company_* / platinum_* are populated for boards listed in the canonical index.
  • Build a board producing both raw .img.xz and .img.qcow2.xz and confirm two distinct manifest entries with different file_extension and redi_url.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added three new action inputs for customizable download base URL, repository, and index URL configuration
    • Introduced automated release asset manifest generation with per-branch asset inventories, metadata enrichment, and missing asset safeguards

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • Needs review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: edab4e3a-fc13-4f29-bd99-4d895e1e1baa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds three new action inputs (armbian_download_base_url, armbian_download_repository, armbian_index_url) and a workflow step that runs an inline Python script to generate per-suffix release asset manifests, enriching image metadata with the provided URLs and writing assets-<suffix>.json files.

Changes

Manifest Generation Feature

Layer / File(s) Summary
Configuration Inputs
action.yml (lines 90–104)
Adds inputs armbian_download_base_url, armbian_download_repository, and armbian_index_url with descriptions, defaults, and optional flags.
Environment / Wiring
action.yml (lines 239–486)
Inserts new workflow step "Generate release assets manifest" that sets env vars from the new inputs and prepares variables consumed by the inline script.
Implementation (inline script)
action.yml (inline Python block within the new step)
Inline Python reads image data and local metadata, optionally fetches/enriches metadata from ARMBIAN_INDEX_URL, constructs download URIs using ARMBIAN_DOWNLOAD_BASE_URL and ARMBIAN_DOWNLOAD_REPOSITORY, builds per-suffix asset lists, logs progress, and writes assets-<suffix>.json with guards for missing assets.
Outputs / Artifacts
action.yml (new step outputs/files)
Writes per-suffix assets-<suffix>.json files into the workspace for downstream steps or release use.
sequenceDiagram
    participant GH as GitHub Actions
    participant Script as Inline Python Script
    participant Index as ARMBIAN_INDEX_URL
    participant FS as Workspace Filesystem

    GH->>Script: start step with env (ARMBIAN_DOWNLOAD_BASE_URL, ARMBIAN_DOWNLOAD_REPOSITORY, ARMBIAN_INDEX_URL)
    Script->>FS: read image data and local metadata files
    alt ARMBIAN_INDEX_URL provided
        Script->>Index: fetch index metadata
        Index-->>Script: return metadata JSON
    end
    Script->>Script: enrich image entries, build download URIs
    Script->>FS: write assets-<suffix>.json files
    Script-->>GH: exit status and logs
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 I hopped through inputs, URLs, and file,
Python stitched manifests with a cheerful smile,
Branch by branch the assets found their place,
Small JSON burrows, tidy and ace,
A rabbit's joy in every release race.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding asset manifest JSON emission to release uploads in the GitHub Action.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch action-release-assets-manifest

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@github-actions github-actions Bot added 05 Milestone: Second quarter release size/medium PR with more then 50 and less then 250 lines Needs review Seeking for review labels May 6, 2026
Copy link
Copy Markdown
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@action.yml`:
- Around line 254-256: The ARMBIAN_DOWNLOAD_REPOSITORY value is used raw and can
contain leading/trailing slashes, which breaks composed URLs (e.g.,
file_url/redi_url); normalize it by trimming both leading and trailing slashes
when reading the env var (assign download_repo =
os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/") ) and apply
the same normalization wherever download_repo is recomputed or used later (also
update the occurrences around the block referenced at lines ~329-337) so URL
composition uses a clean repository segment.
- Around line 306-313: The current candidate-selection logic (variables
candidates, EXTS, and the IMAGES_DIR.glob loop) picks the first-seen extension
rather than the smallest file; change it to group files by stem and choose the
file with the smallest actual disk size (use file.stat().st_size) for each stem
instead of relying on EXTS order. Specifically, iterate all files from
IMAGES_DIR.glob for all EXTS, compute stem as before, and for each stem compare
sizes and replace the chosen extension when a smaller file is found so
candidates[stem] always points to the extension with the smallest file on disk.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06b07d4e-24f5-400b-8c3a-54c693d0b128

📥 Commits

Reviewing files that changed from the base of the PR and between 52ae707 and afaaa8a.

📒 Files selected for processing (1)
  • action.yml

Comment thread action.yml Outdated
Comment thread action.yml Outdated
@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch from afaaa8a to cc05455 Compare May 6, 2026 07:08
igorpecovnik added a commit to armbian/sdk that referenced this pull request May 6, 2026
…-URL manifest

Two changes that go together:

1. Switch the action ref from `armbian/build@main` to
   `armbian/build@action-release-assets-manifest` so this PR's
   aggregator can be tested end-to-end against the unmerged manifest
   feature in armbian/build#9772. Reverts to @main once #9772 lands.

2. Set `armbian_download_base_url` to the GitHub release URL for
   this run, and `armbian_download_repository` to empty. Empty
   repository selects the flat URL shape '<base>/<filename>' (added
   in #9772) instead of the dl.armbian.com '<base>/<board>/<repo>/
   <filename>' shape, and clears the redi_url* fields since GitHub
   releases have no friendly-redirect convention.

   Result: per-cell manifests now carry the *actual* download URL
   for SDK images instead of an aspirational dl.armbian.com path.
Copy link
Copy Markdown
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@action.yml`:
- Around line 374-380: The JSON/YAML currently emits boolean-like fields as
strings (e.g., "promoted": "false", "platinum": "false", "platinum_expired":
"false"), causing consumers to treat them as truthy; change those entries so
they are actual booleans (promoted: false, platinum: false, platinum_expired:
false) or ensure the variables assigned to promoted/platinum/platinum_expired
are boolean values before serialization (use explicit bool() coercion or literal
false), leaving other fields (e.g., download_repository, file_extension,
kernel_version, platinum_until) unchanged.
- Line 277: The warning-print calls that emit f"::warning::Could not parse
{INFO_JSON}: {e}" are writing to stderr so the runner won't process them; update
the print statements that include INFO_JSON (the occurrences currently passing
file=sys.stderr) to write to stdout instead (remove the file=sys.stderr argument
or use file=sys.stdout) so the GitHub Actions workflow command is emitted over
stdout; make the same change for the second similar print (the other occurrence
around the parse failure) and leave the already-correct occurrence that omits
file=sys.stderr unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d4df601b-c845-4569-9411-53f891909893

📥 Commits

Reviewing files that changed from the base of the PR and between afaaa8a and cc05455.

📒 Files selected for processing (1)
  • action.yml

Comment thread action.yml Outdated
Comment thread action.yml Outdated
@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch 2 times, most recently from dcdca83 to 71b96d3 Compare May 6, 2026 07:35
Copy link
Copy Markdown
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.

♻️ Duplicate comments (4)
action.yml (4)

261-261: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize ARMBIAN_DOWNLOAD_REPOSITORY before URL composition.

Line 261 uses the repository segment as-is; leading/trailing slashes will produce malformed paths at Line 379.

Suggested fix
-        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive")
+        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")

Also applies to: 379-379

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 261, Normalize the ARMBIAN_DOWNLOAD_REPOSITORY value
after reading it into download_repo by stripping any leading/trailing slashes so
it cannot introduce duplicate or missing slashes during later URL/path
composition; locate the download_repo assignment and ensure you call .strip("/")
(or equivalent normalization) on that value before it is used to build the
download URL/path (the place where the repo segment is concatenated into the
final URL).

423-423: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize boolean fields as booleans, not "false" strings.

Line 423, Line 427, and Line 428 emit strings, which downstream consumers can treat as truthy values.

Suggested fix
-                "promoted":             "false",
+                "promoted":             False,
...
-                "platinum":             "false",
-                "platinum_expired":     "false",
+                "platinum":             False,
+                "platinum_expired":     False,

Also applies to: 427-428

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 423, The YAML is emitting boolean fields as quoted
strings (e.g. "promoted": "false") which should be real booleans; update the
affected keys (for example promoted and the other two boolean keys shown in the
diff) in action.yml to use unquoted true/false literals (promoted: false, etc.)
so downstream consumers parse them as booleans rather than truthy strings.

350-357: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pick the selected artifact by actual file size, not extension order.

Line 350-357 currently chooses the first-seen extension per stem, which can violate the “most-compressed form” objective when another available format is smaller.

Suggested fix
-        candidates = {}  # stem -> chosen extension
+        EXT_RANK = {ext: i for i, ext in enumerate(EXTS)}  # stable tie-breaker
+        candidates = {}  # stem -> (chosen_ext, chosen_size)
         for ext in EXTS:
             for f in IMAGES_DIR.glob(f"*.{ext}"):
                 stem = f.name[: -(len(ext) + 1)]
-                if stem not in candidates:
-                    candidates[stem] = ext
+                size = f.stat().st_size
+                prev = candidates.get(stem)
+                if (
+                    prev is None
+                    or size < prev[1]
+                    or (size == prev[1] and EXT_RANK[ext] < EXT_RANK[prev[0]])
+                ):
+                    candidates[stem] = (ext, size)

         assets = []
-        for stem, ext in sorted(candidates.items()):
+        for stem, (ext, _) in sorted(candidates.items()):
             img = IMAGES_DIR / f"{stem}.{ext}"

Also applies to: 359-360

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 350 - 357, The current selection logic stores the
first-seen extension per stem (variables: candidates, EXTS, IMAGES_DIR, stem, f)
which can pick a larger file; change it to compare actual file sizes and pick
the smallest file for each stem by using f.stat().st_size (or
Path.stat().st_size) when iterating all matching files, updating candidates to
the extension (or full Path) of the smallest-seen file; apply the same
size-based selection fix to the other similar block referenced (lines 359-360)
so every stem consistently chooses the smallest artifact rather than the first
extension encountered.

308-308: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit ::warning:: workflow commands on stdout, not stderr.

Line 308 and Line 363 send warning commands to sys.stderr; GitHub runner command parsing expects stdout, so annotations may not be created.

Suggested fix
-                print(f"::warning::Could not parse {INFO_JSON}: {e}", file=sys.stderr)
+                print(f"::warning::Could not parse {INFO_JSON}: {e}")
...
-                print(f"::warning::Unrecognised filename {img.name}, skipping", file=sys.stderr)
+                print(f"::warning::Unrecognised filename {img.name}, skipping")

Also applies to: 363-363

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 308, The print statements emitting GitHub workflow
warning commands currently write to stderr (e.g., print(f"::warning::Could not
parse {INFO_JSON}: {e}", file=sys.stderr)); change them to emit on stdout
instead (remove the file=sys.stderr argument or set file=sys.stdout) so the
runner can parse annotations; update both occurrences (the one referencing
INFO_JSON and exception variable e and the other similar print at the second
location) to write to stdout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@action.yml`:
- Line 261: Normalize the ARMBIAN_DOWNLOAD_REPOSITORY value after reading it
into download_repo by stripping any leading/trailing slashes so it cannot
introduce duplicate or missing slashes during later URL/path composition; locate
the download_repo assignment and ensure you call .strip("/") (or equivalent
normalization) on that value before it is used to build the download URL/path
(the place where the repo segment is concatenated into the final URL).
- Line 423: The YAML is emitting boolean fields as quoted strings (e.g.
"promoted": "false") which should be real booleans; update the affected keys
(for example promoted and the other two boolean keys shown in the diff) in
action.yml to use unquoted true/false literals (promoted: false, etc.) so
downstream consumers parse them as booleans rather than truthy strings.
- Around line 350-357: The current selection logic stores the first-seen
extension per stem (variables: candidates, EXTS, IMAGES_DIR, stem, f) which can
pick a larger file; change it to compare actual file sizes and pick the smallest
file for each stem by using f.stat().st_size (or Path.stat().st_size) when
iterating all matching files, updating candidates to the extension (or full
Path) of the smallest-seen file; apply the same size-based selection fix to the
other similar block referenced (lines 359-360) so every stem consistently
chooses the smallest artifact rather than the first extension encountered.
- Line 308: The print statements emitting GitHub workflow warning commands
currently write to stderr (e.g., print(f"::warning::Could not parse {INFO_JSON}:
{e}", file=sys.stderr)); change them to emit on stdout instead (remove the
file=sys.stderr argument or set file=sys.stdout) so the runner can parse
annotations; update both occurrences (the one referencing INFO_JSON and
exception variable e and the other similar print at the second location) to
write to stdout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ae845fdb-5013-47d5-9aab-29e5935cf31d

📥 Commits

Reviewing files that changed from the base of the PR and between cc05455 and 71b96d3.

📒 Files selected for processing (1)
  • action.yml

@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch from 71b96d3 to 14f5ffb Compare May 6, 2026 07:43
@github-actions github-actions Bot added size/large PR with 250 lines or more and removed size/medium PR with more then 50 and less then 250 lines labels May 6, 2026
Copy link
Copy Markdown
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: 1

♻️ Duplicate comments (4)
action.yml (4)

308-308: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit GitHub Actions ::warning:: commands on stdout, not stderr.

These commands are currently printed to sys.stderr, which prevents runner annotation parsing.

Suggested fix
-                print(f"::warning::Could not parse {INFO_JSON}: {e}", file=sys.stderr)
+                print(f"::warning::Could not parse {INFO_JSON}: {e}")
...
-                print(f"::warning::Unrecognised filename {img.name}, skipping", file=sys.stderr)
+                print(f"::warning::Unrecognised filename {img.name}, skipping")

Also applies to: 386-386

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 308, The print statements that emit GitHub Actions
annotations use file=sys.stderr which prevents the runner from parsing them;
update the two calls that look like print(f"::warning::Could not parse
{INFO_JSON}: {e}", file=sys.stderr) (and the similar one at the later
occurrence) to write to stdout instead (e.g., remove the file=sys.stderr
argument or explicitly use file=sys.stdout) so the ::warning:: commands are
emitted on stdout and will be recognized by the Actions runner.

461-466: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize manifest booleans as JSON booleans, not strings.

"false" is a string, not a boolean; downstream truthiness checks can misclassify these entries as enabled.

Suggested fix
-                "promoted":             "false",
+                "promoted":             False,
...
-                "platinum":             "false",
-                "platinum_expired":     "false",
+                "platinum":             False,
+                "platinum_expired":     False,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 461 - 466, The manifest currently emits boolean
fields as strings (e.g., "promoted", "platinum", "platinum_expired" set to
"false"), which breaks downstream truthiness checks; update the manifest
serialization so those keys are emitted as real JSON/YAML booleans (false/true)
instead of quoted strings—locate the block that builds the manifest object
containing "promoted", "download_repository", "file_extension",
"kernel_version", "platinum", and "platinum_expired" and replace the quoted
boolean literals with actual booleans or ensure the serializer writes booleans
(false/true) for those fields.

370-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Select the smallest file by actual size, not extension priority.

The current first-seen rule can violate the “most-compressed form” objective when .zst is smaller than .xz for the same logical image.

Suggested fix
-        candidates = {}  # (stem, image_format) -> chosen extension
+        ext_rank = {ext: i for i, (ext, _) in enumerate(EXTS)}
+        candidates = {}  # (stem, image_format) -> (chosen_ext, chosen_size)
         for ext, image_format in EXTS:
             for f in IMAGES_DIR.glob(f"*.{ext}"):
                 stem = f.name[: -(len(ext) + 1)]
                 key = (stem, image_format)
-                if key not in candidates:
-                    candidates[key] = ext
+                size = f.stat().st_size
+                prev = candidates.get(key)
+                if (
+                    prev is None
+                    or size < prev[1]
+                    or (size == prev[1] and ext_rank[ext] < ext_rank[prev[0]])
+                ):
+                    candidates[key] = (ext, size)

         assets = []
-        for (stem, image_format), ext in sorted(candidates.items()):
+        for (stem, image_format), (ext, _) in sorted(candidates.items()):
             img = IMAGES_DIR / f"{stem}.{ext}"

Also applies to: 382-383

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 370 - 380, The current selection logic around EXTS /
candidates picks the first-seen extension rather than the smallest file; change
it to compare actual file sizes: iterate IMAGES_DIR.glob for all
ext,image_format pairs and for each file compute size = f.stat().st_size, then
for key = (stem, image_format) store the ext only if the key is new or this
file's size is smaller than the currently recorded size. Implement this by
replacing the single candidates dict with either a dict mapping key -> (ext,
size) or a parallel sizes dict and update references to candidates (and the
similar logic later referenced around the same block) to use the chosen ext from
the smallest-size entry.

261-262: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize ARMBIAN_DOWNLOAD_REPOSITORY before composing URLs.

Using download_repo raw allows values like /archive/, which produces malformed URL paths and inconsistent download_repository values in output.

Suggested fix
-        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive")
+        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 261 - 262, The env var value read into download_repo
(from ARMBIAN_DOWNLOAD_REPOSITORY) must be normalized to remove any
leading/trailing slashes before it is used to build URLs or emitted in outputs;
change the code that sets download_repo to strip() or trim slashes (e.g.,
download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY",
"archive").strip("/")) and then use that normalized download_repo when composing
index_url/other URLs and when writing the download_repository field so paths are
consistent and won't produce malformed URLs (leave ARMBIAN_INDEX_URL handling
unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@action.yml`:
- Around line 355-359: The kernel parsing drops pre-release suffixes because it
uses kernel.split("-", 1)[0] (splitting at the first hyphen) after reading
last_token; change that to use rsplit("-", 1)[0] so only the final suffix is
removed (preserving versions like "6.19.0-rc5" or "6.6.0-rc1-sdk"), keeping the
existing last_token logic and file_application assignment (variables: kernel,
last_token, file_application).

---

Duplicate comments:
In `@action.yml`:
- Line 308: The print statements that emit GitHub Actions annotations use
file=sys.stderr which prevents the runner from parsing them; update the two
calls that look like print(f"::warning::Could not parse {INFO_JSON}: {e}",
file=sys.stderr) (and the similar one at the later occurrence) to write to
stdout instead (e.g., remove the file=sys.stderr argument or explicitly use
file=sys.stdout) so the ::warning:: commands are emitted on stdout and will be
recognized by the Actions runner.
- Around line 461-466: The manifest currently emits boolean fields as strings
(e.g., "promoted", "platinum", "platinum_expired" set to "false"), which breaks
downstream truthiness checks; update the manifest serialization so those keys
are emitted as real JSON/YAML booleans (false/true) instead of quoted
strings—locate the block that builds the manifest object containing "promoted",
"download_repository", "file_extension", "kernel_version", "platinum", and
"platinum_expired" and replace the quoted boolean literals with actual booleans
or ensure the serializer writes booleans (false/true) for those fields.
- Around line 370-380: The current selection logic around EXTS / candidates
picks the first-seen extension rather than the smallest file; change it to
compare actual file sizes: iterate IMAGES_DIR.glob for all ext,image_format
pairs and for each file compute size = f.stat().st_size, then for key = (stem,
image_format) store the ext only if the key is new or this file's size is
smaller than the currently recorded size. Implement this by replacing the single
candidates dict with either a dict mapping key -> (ext, size) or a parallel
sizes dict and update references to candidates (and the similar logic later
referenced around the same block) to use the chosen ext from the smallest-size
entry.
- Around line 261-262: The env var value read into download_repo (from
ARMBIAN_DOWNLOAD_REPOSITORY) must be normalized to remove any leading/trailing
slashes before it is used to build URLs or emitted in outputs; change the code
that sets download_repo to strip() or trim slashes (e.g., download_repo =
os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")) and then
use that normalized download_repo when composing index_url/other URLs and when
writing the download_repository field so paths are consistent and won't produce
malformed URLs (leave ARMBIAN_INDEX_URL handling unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d7aeb16-0094-4d6b-9837-fbabf2adb831

📥 Commits

Reviewing files that changed from the base of the PR and between 71b96d3 and 14f5ffb.

📒 Files selected for processing (1)
  • action.yml

Comment thread action.yml
@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch 2 times, most recently from c00cf4d to 752f8e8 Compare May 6, 2026 07:58
Copy link
Copy Markdown
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.

♻️ Duplicate comments (4)
action.yml (4)

316-316: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit ::warning:: workflow commands to stdout, not stderr.

These two warning commands are sent to sys.stderr; runner annotations are only reliably parsed from stdout.

Suggested patch
-                print(f"::warning::Could not parse {INFO_JSON}: {e}", file=sys.stderr)
+                print(f"::warning::Could not parse {INFO_JSON}: {e}")
...
-                print(f"::warning::Unrecognised filename {img.name}, skipping", file=sys.stderr)
+                print(f"::warning::Unrecognised filename {img.name}, skipping")

Also applies to: 394-394

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` at line 316, The warning workflow commands are being written to
stderr; change the print calls that emit the GitHub workflow annotation (e.g.,
the print(f"::warning::Could not parse {INFO_JSON}: {e}", file=sys.stderr)
occurrences) to write to stdout instead (remove the file=sys.stderr argument or
use file=sys.stdout) so the runner can reliably parse the ::warning::
annotations; update both occurrences (around the INFO_JSON parse and the similar
line at the other reported spot).

260-262: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize ARMBIAN_DOWNLOAD_REPOSITORY before composing URLs.

download_repo is used raw, so values like /archive/ generate malformed file_url/redi_url paths.

Suggested patch
-        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive")
+        download_repo = os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")

Also applies to: 419-421

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 260 - 262, The environment value for
ARMBIAN_DOWNLOAD_REPOSITORY (read into download_repo) is not normalized and can
contain leading/trailing slashes, causing malformed URLs; update the code that
reads download_repo to strip leading and trailing slashes (e.g., download_repo =
os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")) and ensure
the same normalization is applied at the other occurrence noted (the block
around the second read at lines ~419-421) so that downstream URL composition for
file_url and redi_url always uses a cleaned repository segment.

378-388: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Select by actual file size, not extension priority.

The current logic uses xz > zst > raw ordering; that can violate the “most-compressed form” requirement when .zst is smaller.

Suggested patch
-        candidates = {}  # (stem, image_format) -> chosen extension
+        EXT_RANK = {ext: i for i, (ext, _) in enumerate(EXTS)}  # tie-breaker
+        candidates = {}  # (stem, image_format) -> (chosen_ext, chosen_size)
         for ext, image_format in EXTS:
             for f in IMAGES_DIR.glob(f"*.{ext}"):
                 stem = f.name[: -(len(ext) + 1)]
                 key = (stem, image_format)
-                if key not in candidates:
-                    candidates[key] = ext
+                size = f.stat().st_size
+                prev = candidates.get(key)
+                if (
+                    prev is None
+                    or size < prev[1]
+                    or (size == prev[1] and EXT_RANK[ext] < EXT_RANK[prev[0]])
+                ):
+                    candidates[key] = (ext, size)

         assets = []
-        for (stem, image_format), ext in sorted(candidates.items()):
+        for (stem, image_format), (ext, _) in sorted(candidates.items()):

Also applies to: 390-391

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 378 - 388, The current selection uses extension
order (EXTS) to pick a candidate into the candidates dict, which breaks the
“most-compressed form” requirement; change the logic in the loop over
EXTS/IMAGES_DIR so you compute file size (f.stat().st_size) for each found file
and store the extension yielding the smallest size per key=(stem, image_format)
instead of first-seen. Keep using the same variables (EXTS, IMAGES_DIR,
candidates, stem, image_format, ext, f) and update the comparison to replace
candidates[key] when a smaller-sized file is found; apply the same size-based
selection to the similar block referenced around lines 390-391.

363-367: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve kernel pre-release suffixes when parsing kernel_version.

Current split removes -rc* segments (6.19.0-rc5 becomes 6.19.0), which corrupts kernel metadata.

Suggested patch
             if "-" in kernel:
                 last_token = kernel.rsplit("-", 1)[1]
-                kernel = kernel.split("-", 1)[0]
-                if last_token.isalpha() and not file_application:
-                    file_application = last_token
+                if last_token.isalpha():
+                    if not file_application:
+                        file_application = last_token
+                    kernel = kernel.rsplit("-", 1)[0]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 363 - 367, The kernel parsing currently strips any
trailing hyphen token (e.g., "-rc5") because it blindly splits on "-" and treats
the last token as a potential file_application; update the logic around the
variables kernel, last_token, and file_application so that you only treat the
last token as file_application when it is purely alphabetic and not a kernel
pre-release (e.g., tokens starting with "rc" or matching /^rc\d+$/); if
last_token is a prerelease marker keep it as part of kernel (do not split),
otherwise set file_application as before. Ensure the modified condition lives in
the same parsing block that handles kernel (the code that currently does
last_token = kernel.rsplit("-", 1)[1] and kernel = kernel.split("-", 1)[0]) so
kernel_version retains "-rc*" suffixes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@action.yml`:
- Line 316: The warning workflow commands are being written to stderr; change
the print calls that emit the GitHub workflow annotation (e.g., the
print(f"::warning::Could not parse {INFO_JSON}: {e}", file=sys.stderr)
occurrences) to write to stdout instead (remove the file=sys.stderr argument or
use file=sys.stdout) so the runner can reliably parse the ::warning::
annotations; update both occurrences (around the INFO_JSON parse and the similar
line at the other reported spot).
- Around line 260-262: The environment value for ARMBIAN_DOWNLOAD_REPOSITORY
(read into download_repo) is not normalized and can contain leading/trailing
slashes, causing malformed URLs; update the code that reads download_repo to
strip leading and trailing slashes (e.g., download_repo =
os.environ.get("ARMBIAN_DOWNLOAD_REPOSITORY", "archive").strip("/")) and ensure
the same normalization is applied at the other occurrence noted (the block
around the second read at lines ~419-421) so that downstream URL composition for
file_url and redi_url always uses a cleaned repository segment.
- Around line 378-388: The current selection uses extension order (EXTS) to pick
a candidate into the candidates dict, which breaks the “most-compressed form”
requirement; change the logic in the loop over EXTS/IMAGES_DIR so you compute
file size (f.stat().st_size) for each found file and store the extension
yielding the smallest size per key=(stem, image_format) instead of first-seen.
Keep using the same variables (EXTS, IMAGES_DIR, candidates, stem, image_format,
ext, f) and update the comparison to replace candidates[key] when a
smaller-sized file is found; apply the same size-based selection to the similar
block referenced around lines 390-391.
- Around line 363-367: The kernel parsing currently strips any trailing hyphen
token (e.g., "-rc5") because it blindly splits on "-" and treats the last token
as a potential file_application; update the logic around the variables kernel,
last_token, and file_application so that you only treat the last token as
file_application when it is purely alphabetic and not a kernel pre-release
(e.g., tokens starting with "rc" or matching /^rc\d+$/); if last_token is a
prerelease marker keep it as part of kernel (do not split), otherwise set
file_application as before. Ensure the modified condition lives in the same
parsing block that handles kernel (the code that currently does last_token =
kernel.rsplit("-", 1)[1] and kernel = kernel.split("-", 1)[0]) so kernel_version
retains "-rc*" suffixes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee9d4e94-0a72-4225-b3df-bc840f84c984

📥 Commits

Reviewing files that changed from the base of the PR and between 14f5ffb and 752f8e8.

📒 Files selected for processing (1)
  • action.yml

@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch from 752f8e8 to 690ef1e Compare May 6, 2026 08:13
igorpecovnik added a commit to armbian/sdk that referenced this pull request May 6, 2026
…-URL manifest

Two changes that go together:

1. Switch the action ref from `armbian/build@main` to
   `armbian/build@action-release-assets-manifest` so this PR's
   aggregator can be tested end-to-end against the unmerged manifest
   feature in armbian/build#9772. Reverts to @main once #9772 lands.

2. Set `armbian_download_base_url` to the GitHub release URL for
   this run, and `armbian_download_repository` to empty. Empty
   repository selects the flat URL shape '<base>/<filename>' (added
   in #9772) instead of the dl.armbian.com '<base>/<board>/<repo>/
   <filename>' shape, and clears the redi_url* fields since GitHub
   releases have no friendly-redirect convention.

   Result: per-cell manifests now carry the *actual* download URL
   for SDK images instead of an aspirational dl.armbian.com path.
igorpecovnik added a commit to armbian/sdk that referenced this pull request May 6, 2026
…-URL manifest

Two changes that go together:

1. Switch the action ref from `armbian/build@main` to
   `armbian/build@action-release-assets-manifest` so this PR's
   aggregator can be tested end-to-end against the unmerged manifest
   feature in armbian/build#9772. Reverts to @main once #9772 lands.

2. Set `armbian_download_base_url` to the GitHub release URL for
   this run, and `armbian_download_repository` to empty. Empty
   repository selects the flat URL shape '<base>/<filename>' (added
   in #9772) instead of the dl.armbian.com '<base>/<board>/<repo>/
   <filename>' shape, and clears the redi_url* fields since GitHub
   releases have no friendly-redirect convention.

   Result: per-cell manifests now carry the *actual* download URL
   for SDK images instead of an aspirational dl.armbian.com path.
igorpecovnik added a commit to armbian/sdk that referenced this pull request May 6, 2026
…-URL manifest

Two changes that go together:

1. Switch the action ref from `armbian/build@main` to
   `armbian/build@action-release-assets-manifest` so this PR's
   aggregator can be tested end-to-end against the unmerged manifest
   feature in armbian/build#9772. Reverts to @main once #9772 lands.

2. Set `armbian_download_base_url` to the GitHub release URL for
   this run, and `armbian_download_repository` to empty. Empty
   repository selects the flat URL shape '<base>/<filename>' (added
   in #9772) instead of the dl.armbian.com '<base>/<board>/<repo>/
   <filename>' shape, and clears the redi_url* fields since GitHub
   releases have no friendly-redirect convention.

   Result: per-cell manifests now carry the *actual* download URL
   for SDK images instead of an aspirational dl.armbian.com path.
@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch 3 times, most recently from d2d7cc0 to 3231ae4 Compare May 6, 2026 09:42
Adds a step to the composite action that walks build/output/images
after build, parses each image filename, reads board metadata from the
build framework's existing output/info/image-info.json, optionally
enriches per-board fields by fetching the canonical
github.armbian.com/armbian-images.json (Bigin-resolved company info,
platinum status, and a fallback for board_* metadata), and writes one
'<image_filename>.assets.json' next to each artefact. The existing
release-action glob picks the manifests up automatically — no caller
change needed.

Schema mirrors the canonical github.armbian.com/armbian-images.json
shape verbatim, so a downstream aggregator can union per-build
fragments into the central index without bespoke parsing.

Filename parsing details (mirrored from
armbian.github.io/scripts/generate-armbian-images-json.sh):

- Variant token can carry an optional '.application' (e.g.
  'minimal.hyperv').
- Kernel field can carry '-suffix'. Strip only the LAST dash-segment
  via rsplit so multi-dash kernels preserve any intermediate tag in
  kernel_version (e.g. '6.6.0-rc1-sdk' -> kernel '6.6.0-rc1', app
  'sdk'). Accept the stripped suffix as file_application iff every
  char is a letter (sdk, omv, kali, homeassistant, openhab, ufs, …)
  so digit-bearing pre-release tags like 'rc5' don't pose as apps.
  Variant-side application takes precedence when both sources have one.

Image format handling:

- Each image file is keyed by (stem, image_format) where image_format
  is one of img, img.qcow2, img.vhdx, hyperv.zip. Same stem with
  multiple formats produces multiple manifest entries — raw and qcow2
  builds of the same logical image are distinct products.
- Per (stem, image_format) the most-compressed form wins (xz > zst >
  raw uncompressed).
- Format suffix shows up in redi_url: 'minimal-qcow2', 'minimal-hyperv'.

Three new optional inputs configure the URL shape and enrichment:

  armbian_download_base_url    default: https://dl.armbian.com
  armbian_download_repository  default: archive
  armbian_index_url            default: https://github.armbian.com/armbian-images.json

armbian_download_repository selects between two URL shapes:

  set         -> {base}/{board}/{repo}/{filename}      dl.armbian.com style
                 redi_url* uses the friendly {distro}_{branch}_{variant}
                 redirect path on dl.armbian.com.
  empty       -> {base}/{filename}                     flat (e.g. GitHub release)
                 redi_url* mirror file_url* since there is no separate
                 redirect convention.

Leading/trailing slashes are stripped from the input so 'archive/',
'/archive/', and a slash-only value (which collapses to '' = flat
mode) all behave correctly.

armbian_index_url is fetched best-effort (timeout 30s, warning on
failure) to build a board_slug -> {board_*, company_*, platinum_*}
lookup. Local image-info.json wins per-field for board_*; company_*
and platinum_* are sourced only from the index. Empty disables
enrichment entirely.

Per-image manifest naming '<image_filename>.assets.json' guarantees
uniqueness across (a) multiple images in this build call (raw +
.img.qcow2 + .img.vhdx all coexist when image-output-* extensions
are enabled) and (b) parallel matrix cells with different extension
lists landing in the same release. Schema stays {"assets": [...]}
so consumers expecting the array (jq -s 'map(.assets) | flatten')
keep working unchanged.

Tested locally against an 18-image build/output/images tree on a
developer host: 18/18 filenames parse, board_name / vendor / support /
introduced populate from image-info.json, company_* + platinum_*
populate from the live published index (340 boards loaded). Synthetic
SDK-style filenames (kernel '6.18.26-sdk') strip cleanly to
kernel_version='6.18.26' / file_application='sdk'. Multi-dash cases
(rc kernel + app) preserve the rc tag. Edge cases (rc5/beta1/alpha2/pre3
without app, digit-suffix detection, hyperv variant suffixes) all
behave correctly.
@igorpecovnik igorpecovnik force-pushed the action-release-assets-manifest branch from 3231ae4 to 8842c5b Compare May 6, 2026 09:47
Copy link
Copy Markdown
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.

♻️ Duplicate comments (2)
action.yml (2)

428-439: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Choose the kept artifact by actual size, not extension order.

Line 433 iterates EXTS in priority order and Line 437 locks in the first match, so this can keep a larger .xz when the .zst is smaller. That breaks the stated "most-compressed form" rule.

Suggested fix
+        EXT_RANK = {ext: i for i, (ext, _) in enumerate(EXTS)}
+
         def collect_candidates():
             """Walk IMAGES_DIR, group files by (stem, image_format). Within
             each group keep the most-compressed form (EXTS is ordered xz >
             zst > raw, first-seen wins)."""
             out = {}
             for ext, image_format in EXTS:
                 for f in IMAGES_DIR.glob(f"*.{ext}"):
                     stem = f.name[: -(len(ext) + 1)]
                     key = (stem, image_format)
-                    if key not in out:
-                        out[key] = ext
-            return out
+                    size = f.stat().st_size
+                    prev = out.get(key)
+                    if (
+                        prev is None
+                        or size < prev[1]
+                        or (size == prev[1] and EXT_RANK[ext] < EXT_RANK[prev[0]])
+                    ):
+                        out[key] = (ext, size)
+            return {key: ext for key, (ext, _) in out.items()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 428 - 439, The collect_candidates() function
currently picks the first-seen extension from EXTS instead of the smallest file,
which can keep a larger .xz over a smaller .zst; change the logic to compare
actual file sizes using f.stat().st_size for each file under IMAGES_DIR and keep
the extension (and/or record) of the smallest-size file per key (stem,
image_format). In practice, for each Path f in IMAGES_DIR.glob, compute size =
f.stat().st_size and update out[key] only if key is new or size < recorded_size
(store size alongside the chosen ext or replace ext when smaller) so the kept
artifact is truly the most-compressed form.

307-311: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve promoted/platinum* as real booleans from enrichment.

promoted is never loaded from the canonical index here, and Lines 476/480/481 serialize fallback values as "false" strings. That means promoted images are always downgraded in the fragment, and downstream consumers will read these fields incorrectly.

Suggested fix
+        BOOLEAN_FIELDS = {"promoted", "platinum", "platinum_expired"}
         ENRICHMENT_FIELDS = (
             "board_name", "board_vendor", "board_support", "board_introduced",
             "company_name", "company_website", "company_logo",
+            "promoted",
             "platinum", "platinum_expired", "platinum_until",
         )
@@
-                if slug and slug not in out:
-                    out[slug] = {k: asset.get(k) or "" for k in ENRICHMENT_FIELDS}
+                if slug and slug not in out:
+                    out[slug] = {
+                        k: asset.get(k, False) if k in BOOLEAN_FIELDS else asset.get(k, "")
+                        for k in ENRICHMENT_FIELDS
+                    }
@@
-                "promoted":            "false",
+                "promoted":            enrich.get("promoted", False),
                 "download_repository": DOWNLOAD_REPO,
                 "file_extension":      ext,
                 "kernel_version":      parsed["kernel_version"],
-                "platinum":            enrich.get("platinum") or "false",
-                "platinum_expired":    enrich.get("platinum_expired") or "false",
+                "platinum":            enrich.get("platinum", False),
+                "platinum_expired":    enrich.get("platinum_expired", False),
                 "platinum_until":      enrich.get("platinum_until", ""),

Also applies to: 327-330, 476-481

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 307 - 311, ENRICHMENT_FIELDS currently omits
"promoted" and downstream fallback assignments serialize boolean-like values as
the string "false", so update the ENRICHMENT_FIELDS tuple to include "promoted"
and change the enrichment-loading/serialization logic that writes fallback
values (the code paths that set promoted, platinum, platinum_expired,
platinum_until when enrichment is missing) to emit real booleans/None (e.g.,
promoted: False, platinum: False, platinum_expired: False, platinum_until: None)
and normalize any incoming string "true"/"false" to Python booleans when reading
enrichment; look for the ENRICHMENT_FIELDS constant and the functions that build
the fragment/fallback values and replace string literals "false"/"true" with
proper boolean/None conversions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@action.yml`:
- Around line 428-439: The collect_candidates() function currently picks the
first-seen extension from EXTS instead of the smallest file, which can keep a
larger .xz over a smaller .zst; change the logic to compare actual file sizes
using f.stat().st_size for each file under IMAGES_DIR and keep the extension
(and/or record) of the smallest-size file per key (stem, image_format). In
practice, for each Path f in IMAGES_DIR.glob, compute size = f.stat().st_size
and update out[key] only if key is new or size < recorded_size (store size
alongside the chosen ext or replace ext when smaller) so the kept artifact is
truly the most-compressed form.
- Around line 307-311: ENRICHMENT_FIELDS currently omits "promoted" and
downstream fallback assignments serialize boolean-like values as the string
"false", so update the ENRICHMENT_FIELDS tuple to include "promoted" and change
the enrichment-loading/serialization logic that writes fallback values (the code
paths that set promoted, platinum, platinum_expired, platinum_until when
enrichment is missing) to emit real booleans/None (e.g., promoted: False,
platinum: False, platinum_expired: False, platinum_until: None) and normalize
any incoming string "true"/"false" to Python booleans when reading enrichment;
look for the ENRICHMENT_FIELDS constant and the functions that build the
fragment/fallback values and replace string literals "false"/"true" with proper
boolean/None conversions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 181d59ba-5d38-48e0-ac20-6a33ce7bc6f7

📥 Commits

Reviewing files that changed from the base of the PR and between 14f5ffb and 8842c5b.

📒 Files selected for processing (1)
  • action.yml

igorpecovnik added a commit to armbian/sdk that referenced this pull request May 6, 2026
)

* ci: aggregate per-cell asset manifests into armbian-sdk-images.json

The build action (armbian/build PR #9772) now writes a per-cell
`assets-<board>-<release>-<branch>-<ui>.json` next to each Matrix
cell's image upload. With 8 cells (board × os × extension) the
release ends up with 8 small JSONs and one logical inventory split
across them.

Add a final Aggregate job that runs after Matrix completes,
downloads every `assets-*.json` from the release, and merges them
into a single `armbian-sdk-images.json` uploaded back to the same
release. Downstream consumers (and the central index server at
github.armbian.com) get one canonical inventory file per release
instead of having to enumerate per-cell fragments.

The existing per-cell manifests are left in place so individual
cells remain inspectable; only the merged file is created on top.

`if: always() && !cancelled()` lets the aggregator still merge
manifests for the cells that did succeed when one matrix cell
fails — fail-fast: false is already set on the Matrix strategy.

* ci: pin to armbian/build PR #9772 branch for testing + configure flat-URL manifest

Two changes that go together:

1. Switch the action ref from `armbian/build@main` to
   `armbian/build@action-release-assets-manifest` so this PR's
   aggregator can be tested end-to-end against the unmerged manifest
   feature in armbian/build#9772. Reverts to @main once #9772 lands.

2. Set `armbian_download_base_url` to the GitHub release URL for
   this run, and `armbian_download_repository` to empty. Empty
   repository selects the flat URL shape '<base>/<filename>' (added
   in #9772) instead of the dl.armbian.com '<base>/<board>/<repo>/
   <filename>' shape, and clears the redi_url* fields since GitHub
   releases have no friendly-redirect convention.

   Result: per-cell manifests now carry the *actual* download URL
   for SDK images instead of an aspirational dl.armbian.com path.

* ci: fix typo armbian_release_tittle -> armbian_release_title

Pre-existing typo (double 't') made the SDK release title silently
fall back to the build action's default 'Armbian image' instead of
'Armbian SDK'. Surfaced as a GitHub Actions warning:

  Warning: Unexpected input(s) 'armbian_release_tittle',
  valid inputs are [..., 'armbian_release_title', ...]

Same fix in the inline comment.

* ci: switch aggregator pattern to *.assets.json (per-image manifest)

Pairs with the build action's switch from one combined manifest per
matrix cell to one manifest per image file. Per-cell naming
('assets-<board>-<release>-<branch>-<ui>.json') collided when two
matrix cells differed only in their extension list (e.g. one adds
image-output-qcow2, the other doesn't) — both wrote the same name
and the later upload silently overwrote the earlier.

Per-image naming ('<image_filename>.assets.json') is guaranteed
unique across cells because image filenames are. The aggregator
just needs the matching glob.

* ci: name aggregated manifest armbian-images.json

Match the canonical name used by the central index at
github.armbian.com/armbian-images.json so consumers can use the same
filename across sources.

Was applied previously but lost during a branch cleanup; reapplying.
ncipollo/release-action defaults artifactErrorsFailBuild to false, so
GitHub's 2 GiB per-asset cap (and other upload failures) only surfaced
as warnings while the job reported success. Flip it so over-size or
broken uploads fail the job loudly.
Wires through to ncipollo/release-action's prerelease flag so matrix
builds can keep the release in pre-release state while images are
still uploading, then promote it once the post-matrix aggregator
finishes. Defaults to false to preserve current behaviour for callers
that don't opt in.
@igorpecovnik igorpecovnik added Ready to merge Reviewed, tested and ready for merge and removed Needs review Seeking for review labels May 6, 2026
@igorpecovnik igorpecovnik merged commit 7d91e37 into main May 6, 2026
12 checks passed
@igorpecovnik igorpecovnik deleted the action-release-assets-manifest branch May 6, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

05 Milestone: Second quarter release Ready to merge Reviewed, tested and ready for merge size/large PR with 250 lines or more

Development

Successfully merging this pull request may close these issues.

1 participant