Skip to content

Restore release signing via CI-injected keystore (our own key)#48

Merged
hawkff merged 4 commits into
mainfrom
chore/release-signing
Jun 20, 2026
Merged

Restore release signing via CI-injected keystore (our own key)#48
hawkff merged 4 commits into
mainfrom
chore/release-signing

Conversation

@hawkff

@hawkff hawkff commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Restores release signing after the inherited release.keystore was removed from the repo
(PR #44), using a CI-secret-injected keystore and our own freshly generated key.

Background

release.keystore (inherited from the upstream fork) was removed from HEAD and gitignored, but
nothing recreated it at build time — so assembleOssRelease (build.yml / release.yml) would fail
at packaging on the missing file. PR CI stayed green only because it builds debug. We're now
maintaining our own fork, so this uses a new key we generated.

Changes

  • buildSrc/Helpers.kt — only enable release signing when the keystore file exists AND a
    non-blank password is provided. Debug / PR / keyless builds skip signing cleanly instead of
    failing on the missing file (empty-string CI secrets count as absent).
  • .github/workflows/build.yml, release.yml — decode release.keystore from a new
    KEYSTORE_B64 secret before assembleOssRelease (base64 -d), guarded so it's a no-op when
    the secret is unset.
  • .gitignore — explicitly ignore buildSrc/build (it's sometimes a symlink to APFS /tmp as
    the exFAT AppleDouble build-failure workaround; the build/ rule doesn't match a symlink).

Key + secrets (handled out of band)

  • Generated a fresh 4096-bit RSA key, CN=NekoBox, OU=hawkff, 30y validity, stored locally at
    ~/.nekobox-signing/ (mode 600, never committed).
  • Set repo secrets: KEYSTORE_B64, KEYSTORE_PASS, ALIAS_NAME, ALIAS_PASS.
  • The old inherited key is left in git history (no force-push); it is unused going forward.

Testing

  • CodeRabbit CLI: No findings.
  • Signing verified end-to-end locally: assembleOssRelease produces
    NekoBox-…-arm64-v8a.apk signed with our key — apksigner verify shows
    CN=NekoBox, OU=hawkff and cert SHA-256 13a113a3…5865add, matching the keystore fingerprint.
  • Debug build unaffected (skips signing).

Caveat (separate, pre-existing — not in this PR)

assembleOssRelease currently also fails lint on a pre-existing MissingDefaultResource
error (dracula_background in values-night/colors.xml, from the Dracula theme #38) — unrelated
to signing. That should be fixed separately before a real release build, or it will block
build.yml/release.yml regardless of signing.

Greptile Summary

This PR restores release APK signing by decoding the keystore from a KEYSTORE_B64 CI secret at build time and tightening the Gradle signing guard to require all four credentials to be present and non-blank before wiring up a signing config.

  • buildSrc/Helpers.kt: replaces the loose keystorePwd != null check with a canSign guard that verifies the keystore file exists and that KEYSTORE_PASS, ALIAS_NAME, and ALIAS_PASS are all non-blank — addressing the null-alias crash flagged in the prior review.
  • build.yml / release.yml: adds a guarded base64 -d step to materialize release.keystore from KEYSTORE_B64 before the Gradle release task, with a no-op when the secret is absent; also corrects the ${{ secrets.ALIAS_PASS }} spacing inconsistency from the previous code.
  • .gitignore: explicitly ignores buildSrc/build to cover the macOS exFAT symlink edge-case where the generic build/ rule does not match a symlink.

Confidence Score: 5/5

Safe to merge — the signing guard is correct and complete, the keystore decode is a no-op when the secret is absent, and all issues from prior review rounds have been resolved.

All four credentials are now validated before a signing config is created, preventing the null-alias build failure. The base64 -d decode is properly guarded by a non-empty check, so PR/debug builds without the secret continue to work. No new defects were introduced.

No files require special attention.

Important Files Changed

Filename Overview
buildSrc/src/main/kotlin/Helpers.kt Replaced the single-condition keystorePwd != null guard with a four-condition canSign check (file exists, password, alias, and alias-password all non-blank), fixing both the missing-file crash and the null alias issue flagged in prior review.
.github/workflows/build.yml Adds a guarded base64 -d decode step for KEYSTORE_B64 before the release Gradle task; also fixes the missing space in ${{ secrets.ALIAS_PASS }} from the old code.
.github/workflows/release.yml Identical signing-bootstrap changes as build.yml; the ALIAS_PASS spacing fix is also applied here.
.gitignore Adds explicit buildSrc/build entry to handle the macOS exFAT symlink edge-case where the generic trailing-slash build/ rule does not match a symlink.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GH as GitHub Actions
    participant Shell as Runner Shell
    participant Gradle as Gradle / Helpers.kt
    participant APK as APK Signer

    GH->>Shell: Expand secrets (KEYSTORE_B64, KEYSTORE_PASS, ALIAS_NAME, ALIAS_PASS)
    Shell->>Shell: if [ -n "$KEYSTORE_B64" ]
    alt secret present
        Shell->>Shell: "echo base64 | base64 -d > release.keystore"
    else secret absent
        Shell->>Shell: skip (no-op)
    end
    Shell->>Gradle: assembleOssRelease
    Gradle->>Gradle: "canSign = file.exists() && !pwd.isNullOrBlank() && !alias.isNullOrBlank() && !aliasPwd.isNullOrBlank()"
    alt "canSign = true"
        Gradle->>Gradle: create signingConfig("release")
        Gradle->>APK: sign APK with release key
    else "canSign = false"
        Gradle->>APK: produce unsigned APK
    end
    APK-->>GH: upload artifact
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GH as GitHub Actions
    participant Shell as Runner Shell
    participant Gradle as Gradle / Helpers.kt
    participant APK as APK Signer

    GH->>Shell: Expand secrets (KEYSTORE_B64, KEYSTORE_PASS, ALIAS_NAME, ALIAS_PASS)
    Shell->>Shell: if [ -n "$KEYSTORE_B64" ]
    alt secret present
        Shell->>Shell: "echo base64 | base64 -d > release.keystore"
    else secret absent
        Shell->>Shell: skip (no-op)
    end
    Shell->>Gradle: assembleOssRelease
    Gradle->>Gradle: "canSign = file.exists() && !pwd.isNullOrBlank() && !alias.isNullOrBlank() && !aliasPwd.isNullOrBlank()"
    alt "canSign = true"
        Gradle->>Gradle: create signingConfig("release")
        Gradle->>APK: sign APK with release key
    else "canSign = false"
        Gradle->>APK: produce unsigned APK
    end
    APK-->>GH: upload artifact
Loading

Reviews (3): Last reviewed commit: "chore(ci): normalize spacing in ALIAS_PA..." | Re-trigger Greptile

hawkff added 2 commits June 20, 2026 18:11
…when absent

The inherited release.keystore was removed from the repo (gitignored). Wire CI to
recreate it from a new KEYSTORE_B64 secret (decoded to release.keystore before
assembleOssRelease in build.yml/release.yml), and make Helpers.kt only enable release
signing when the keystore file exists AND a non-blank password is set, so debug / PR /
keyless builds skip signing instead of failing on the missing file.

Signing verified end-to-end locally with a freshly generated 4096-bit RSA key
(CN=NekoBox, OU=hawkff): assembleOssRelease produces an APK signed with our key
(apksigner cert SHA-256 matches the keystore). GitHub secrets KEYSTORE_B64/
KEYSTORE_PASS/ALIAS_NAME/ALIAS_PASS set on the repo.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR introduces conditional release signing: Helpers.kt now guards signingConfigs creation with both a non-blank KEYSTORE_PASS and the physical existence of release.keystore. Both CI workflows (build.yml, release.yml) add a step that base64-decodes the KEYSTORE_B64 secret into release.keystore before the Gradle build. .gitignore gains an explicit buildSrc/build pattern.

Changes

Keystore Secret Provisioning and Signing Guard

Layer / File(s) Summary
Conditional signing config guard
buildSrc/src/main/kotlin/Helpers.kt
setupAppCommon() adds a canSign predicate that requires both a non-blank KEYSTORE_PASS and the existence of release.keystore; the signingConfigs { create("release") } block is now conditional on canSign, replacing the previous null-only check.
CI keystore generation from secret
.github/workflows/build.yml, .github/workflows/release.yml, .gitignore
Both CI workflows add a guarded inline step that base64-decodes KEYSTORE_B64 into release.keystore before invoking the Gradle assemble task. .gitignore adds an explicit buildSrc/build rule with comments explaining an APFS/exFAT AppleDouble symlink edge case.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A secret arrives, base64 in hand,
Decoded to keystore, right where Gradle planned.
canSign checks the file before it proceeds,
No phantom configs sprouting like weeds.
The rabbit hops safely, the APK's sealed! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: restoring release signing via CI-injected keystore, which is the core objective of this PR.
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 The PR description comprehensively details the purpose (restoring release signing), implementation changes (keystore decode, signing guard, gitignore update), and testing (end-to-end signing verification).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

Comment thread buildSrc/src/main/kotlin/Helpers.kt Outdated
Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/release.yml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@buildSrc/src/main/kotlin/Helpers.kt`:
- Line 127: The canSign variable on line 127 only validates keystorePwd and
releaseKeystore existence, but the signing configuration block unconditionally
uses alias and pwd variables which may be null or blank, causing cryptic Gradle
failures. Update the canSign predicate to also check that alias and pwd are not
null or blank, ensuring all required signing credentials are validated before
proceeding with the signing configuration.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea1696af-9a71-4fbc-b347-3c5d8e6fe522

📥 Commits

Reviewing files that changed from the base of the PR and between 97581c4 and 6fdfc82.

📒 Files selected for processing (4)
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • .gitignore
  • buildSrc/src/main/kotlin/Helpers.kt

Comment thread buildSrc/src/main/kotlin/Helpers.kt Outdated
hawkff added 2 commits June 20, 2026 18:19
…ord)

Per Greptile: canSign only checked the keystore file + store password, so if
ALIAS_NAME/ALIAS_PASS were absent while the others were present, Gradle would build a
signing config with null alias/password and fail at packaging with an opaque error.
Require all of keystore-exists + storePass + alias + keyPass non-blank before enabling
release signing; otherwise skip cleanly.
@hawkff hawkff merged commit d0c3b1d into main Jun 20, 2026
5 checks passed
@hawkff hawkff deleted the chore/release-signing branch June 20, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant