Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Codecov Configuration for RepForge (Devasy/RepForge)
codecov:
require_ci_to_pass: yes

coverage:
precision: 2
round: down
range: "70...100"

status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: auto

ignore:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "workout-logger/test/**/*"
19 changes: 15 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- main
workflow_dispatch:

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Build and Release APK
Expand All @@ -18,17 +22,20 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow pins =="
grep -nE 'uses: [^[:space:]]+@' .github/workflows/release.yml .github/workflows/test.yml 2>/dev/null || true

echo
echo "== tracked workflow files =="
git ls-files .github/workflows || true

Repository: Devasy/RepForge

Length of output: 907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re
for path in [Path(".github/workflows/release.yml"), Path(".github/workflows/test.yml")]:
    print(f"\n== {path} ==")
    for i, line in enumerate(path.read_text().splitlines(), 1):
        m = re.search(r'uses:\s*([^\s#]+)@([^\s#]+)', line)
        if m:
            ref = m.group(2)
            status = "SHA-like" if re.fullmatch(r'[0-9a-fA-F]{40}', ref) else "ref-like"
            print(f"{i}: {m.group(1)}@{m.group(2)} :: {status}")
PY

Repository: Devasy/RepForge

Length of output: 601


Pin GitHub Actions to immutable commit SHAs. Mutable tags/branches let upstream changes alter release or test workflow execution. Pin the checked-in action references to reviewed commit SHAs, including actions/checkout, actions/setup-java, gradle/actions/setup-gradle, subosito/flutter-action, actions/upload-artifact, softprops/action-gh-release, and codecov/codecov-action.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

📍 Affects 2 files
  • .github/workflows/release.yml#L25-L25 (this comment)
  • .github/workflows/release.yml#L31-L31
  • .github/workflows/release.yml#L37-L37
  • .github/workflows/release.yml#L164-L164
  • .github/workflows/test.yml#L25-L25
🤖 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 @.github/workflows/release.yml at line 25, Pin every listed GitHub Action to
a reviewed immutable commit SHA instead of a mutable tag or branch. Update
actions/checkout, actions/setup-java, gradle/actions/setup-gradle,
subosito/flutter-action, actions/upload-artifact, softprops/action-gh-release,
and codecov/codecov-action at all affected sites: .github/workflows/release.yml
lines 25-25, 31-31, 37-37, and 164-164, plus .github/workflows/test.yml line
25-25.

Source: Linters/SAST tools

with:
fetch-depth: 0 # Fetch all history for proper versioning
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Gradle Build Cache
uses: gradle/actions/setup-gradle@v6

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
Expand Down Expand Up @@ -117,6 +124,10 @@ jobs:

- name: Decode release keystore
run: |
if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ]; then
echo "Error: KEYSTORE_BASE64 secret is not configured in repository secrets."
exit 1
fi
Comment on lines +127 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail releases when any signing credential is absent. The workflow validates only KEYSTORE_BASE64, while Gradle leaves release signing unset when any password or alias is blank and silently signs with the debug key.

  • .github/workflows/release.yml#L127-L130: validate KEYSTORE_BASE64, KEY_STORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD before building.
  • workout-logger/android/app/build.gradle.kts#L44-L48: treat blank environment values as absent before falling back to key.properties.
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 127-127: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

📍 Affects 2 files
  • .github/workflows/release.yml#L127-L130 (this comment)
  • workout-logger/android/app/build.gradle.kts#L44-L48
🤖 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 @.github/workflows/release.yml around lines 127 - 130, Ensure release signing
fails when any credential is missing: in .github/workflows/release.yml lines
127-130, validate KEYSTORE_BASE64, KEY_STORE_PASSWORD, KEY_ALIAS, and
KEY_PASSWORD before building; in workout-logger/android/app/build.gradle.kts
lines 44-48, treat blank environment values as absent before falling back to
key.properties, preventing incomplete credentials from selecting debug signing.

echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > /tmp/repforge-release.jks

- name: Build APK
Expand All @@ -126,7 +137,7 @@ jobs:
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: flutter build apk --release --split-per-abi
run: flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/app/outputs/symbols

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retain Dart and R8 deobfuscation artifacts.

This runner is ephemeral and later steps only publish APKs, so production stack traces cannot be decoded. Upload build/app/outputs/symbols and android/app/build/outputs/mapping/release/mapping.txt as a private retained artifact.

🤖 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 @.github/workflows/release.yml at line 140, Update the release workflow step
containing the Flutter APK build to retain both Dart symbols from
build/app/outputs/symbols and the R8 mapping file at
android/app/build/outputs/mapping/release/mapping.txt. Add a private artifact
upload with retention configured so these deobfuscation files are preserved
alongside the published APKs.


- name: Rename APKs
run: |
Expand All @@ -150,7 +161,7 @@ jobs:

- name: Create GitHub Release
if: github.event_name == 'push' && steps.commit_version.outputs.committed == 'true'
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ steps.version.outputs.value }}
name: RepForge v${{ steps.version.outputs.value }}
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ on:
release:
types: [published]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
name: Analyze & Test
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Flutter
id: flutter-action
Expand All @@ -28,13 +35,14 @@ jobs:
pub-cache-key: "flutter-pub-:os:-:channel:-:version:-:arch:-${{ hashFiles('workout-logger/pubspec.lock') }}"

- name: Install dependencies
if: steps.flutter-action.outputs.PUB-CACHE-HIT != 'true'
if: steps.flutter-action.outputs.CACHE-HIT != 'true'
working-directory: ./workout-logger
run: flutter pub get
Comment on lines 37 to 40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files .github/workflows || true

echo "== test workflow excerpt =="
if [ -f .github/workflows/test.yml ]; then
  nl -ba .github/workflows/test.yml | sed -n '1,140p'
fi

echo "== flutter-action/cache references =="
rg -n "flutter-action|CACHE-HIT|flutter pub get|pub get|dart_tool/package_config|flutter analyze|flutter test" .github/workflows workout-logger 2>/dev/null || true

echo "== flutter action usage context in tracked files =="
for f in $(git ls-files .github/workflows); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,220p'
done

Repository: Devasy/RepForge

Length of output: 299


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpts =="
for f in .github/workflows/release.yml .github/workflows/test.yml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    awk '{printf "%6d  %s\n", NR, $0}' "$f" | sed -n '1,220p'
  fi
done

echo "== search relevant references =="
rg -n "flutter-action|CACHE-HIT|flutter pub get|pub get|dart_tool/package_config|flutter analyze|flutter test|subosito" .github/workflows workout-logger 2>/dev/null || true

Repository: Devasy/RepForge

Length of output: 11942


🌐 Web query:

subosito flutter-action v2 cache true CACHE-HIT output generated files .dart_tool/package_config.json

💡 Result:

The subosito/flutter-action action (v2) with cache: true enabled does not automatically cache the.dart_tool/package_config.json file or your project's generated files [1][2][3]. When you enable cache: true, the action uses actions/cache internally to cache two specific areas [4]: 1. The Flutter SDK installation [1][4]. 2. The Dart/Flutter pub package cache (commonly located in ~/.pub-cache) [1][4][5]. The CACHE-HIT and PUB-CACHE-HIT outputs provided by the action reflect whether an exact match was found for these specific SDK and pub caches, respectively [2][6][3]. They do not indicate the presence or validity of your project's local build artifacts like.dart_tool/package_config.json [2][7]. If you need to persist generated files (such as those in.dart_tool,.g.dart files, etc.) across workflow runs, you must explicitly add a separate actions/cache step to your workflow configuration [8]. Example of how to manually cache these files: - name: Cache generated files uses: actions/cache@v4 with: path: |.dart_tool/package_config.json /.dart_tool key: build-artifacts-${{ hashFiles('/pubspec.lock') }} [8] The CACHE-HIT output from subosito/flutter-action is solely for the action's own internal caching logic [2][9]. You should not rely on it as a proxy for the state of your application's generated build artifacts [2][7].

Citations:


Run flutter pub get on every checkout.

A cache hit from subosito/flutter-action restores the Flutter package cache/SDK, not the checkout’s generated .dart_tool/package_config.json; skipping flutter pub get on cache hits can make analyze or test fail.

Proposed fix
       - name: Install dependencies
-        if: steps.flutter-action.outputs.CACHE-HIT != 'true'
         working-directory: ./workout-logger
         run: flutter pub get
📝 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
- name: Install dependencies
if: steps.flutter-action.outputs.PUB-CACHE-HIT != 'true'
if: steps.flutter-action.outputs.CACHE-HIT != 'true'
working-directory: ./workout-logger
run: flutter pub get
- name: Install dependencies
working-directory: ./workout-logger
run: flutter pub get
🤖 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 @.github/workflows/test.yml around lines 37 - 40, Update the dependency
installation step in the workflow to run flutter pub get on every checkout by
removing the CACHE-HIT condition from the step using the flutter-action output.
Preserve its working-directory and command.


- name: Analyze
working-directory: ./workout-logger
run: |
set -o pipefail
# Only fail on errors, ignore warnings and info messages
flutter analyze --no-fatal-infos --no-fatal-warnings | tee analyze_output.txt

Expand All @@ -53,3 +61,5 @@ jobs:
with:
files: workout-logger/coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
slug: Devasy/RepForge

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the trailing blank line.

YAMLlint reports this as an error, so the workflow lint check will fail.

🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 64-64: too many blank lines (1 > 0)

(empty-lines)

🤖 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 @.github/workflows/test.yml at line 64, Remove the trailing blank line at the
end of the workflow file so the YAML lint check passes.

Source: Linters/SAST tools

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,9 @@ repforge_backup_*.json

# Claude Code project memory & session files
.claude/

# Hive test databases and temporary directories
*.hive
tmp_hive_*/
**/tmp_hive_*/

16 changes: 16 additions & 0 deletions workout-logger/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

# Signing Keystore & Credentials
**/android/key.properties
*.jks
*.keystore
*.p12
.env
.env.*

# Hive test databases and temporary directories
*.hive
tmp_hive_*/
**/tmp_hive_*/



23 changes: 18 additions & 5 deletions workout-logger/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
import java.io.FileInputStream
import java.util.Properties

plugins {
id("com.android.application")
Expand Down Expand Up @@ -34,11 +36,16 @@ android {

signingConfigs {
create("release") {
val keystorePath = System.getenv("KEYSTORE_PATH")
val storePass = System.getenv("KEY_STORE_PASSWORD")
val alias = System.getenv("KEY_ALIAS")
val keyPass = System.getenv("KEY_PASSWORD")
if (keystorePath != null && storePass != null && alias != null && keyPass != null) {
val keyProperties = Properties()
val keyPropertiesFile = rootProject.file("key.properties")
if (keyPropertiesFile.exists()) {
keyProperties.load(FileInputStream(keyPropertiesFile))
}
val keystorePath = System.getenv("KEYSTORE_PATH") ?: keyProperties.getProperty("storeFile")
val storePass = System.getenv("KEY_STORE_PASSWORD") ?: keyProperties.getProperty("storePassword")
val alias = System.getenv("KEY_ALIAS") ?: keyProperties.getProperty("keyAlias")
val keyPass = System.getenv("KEY_PASSWORD") ?: keyProperties.getProperty("keyPassword")
if (!keystorePath.isNullOrEmpty() && !storePass.isNullOrEmpty() && !alias.isNullOrEmpty() && !keyPass.isNullOrEmpty()) {
storeFile = file(keystorePath)
storePassword = storePass
keyAlias = alias
Expand Down Expand Up @@ -72,6 +79,12 @@ android {
manifestPlaceholders["appLabel"] = "RepForge (Debug)"
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Uses the production EC P-256 keystore when KEYSTORE_PATH env var is set
// (CI injects it via GitHub Secrets). Falls back to the debug key for a
// local `flutter run --release` without env vars configured.
Expand Down
14 changes: 14 additions & 0 deletions workout-logger/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Suppress missing class warnings for Play Core deferred components in Flutter engine
-dontwarn com.google.android.play.core.**

# Flutter Wrapper Rules
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.** { *; }
-keep class io.flutter.provider.** { *; }
-keep class io.flutter.plugin.editing.** { *; }

# Keep Native plugins and Health Connect interfaces
-dontwarn com.google.android.gms.**
8 changes: 8 additions & 0 deletions workout-logger/android/key.properties.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Local Android Release Keystore Configuration
# Fill in your local keystore path and passwords below.
# Note: This file should NEVER be committed to Git.

storeFile=C:/path/to/your/upload-keystore.jks
storePassword=your_store_password
keyAlias=your_key_alias
keyPassword=your_key_password
35 changes: 23 additions & 12 deletions workout-logger/lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'widgets/readiness_card.dart';
import 'widgets/sleep_hr_card.dart';
import 'widgets/heart_rate_card.dart';
import 'widgets/rf_widgets.dart';
import 'widgets/floating_nav_bar.dart';
import 'widgets/sparkline_painter.dart';
import 'widgets/activity_heatmap.dart';
import 'widgets/body_heatmap.dart';
Expand All @@ -39,19 +40,34 @@ class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;

static const _navItems = [
RFNavItem(icon: Icons.home_rounded, label: 'Home'),
RFNavItem(icon: Icons.layers_rounded, label: 'Routines'),
RFNavItem(icon: Icons.history_rounded, label: 'History'),
RFNavItem(icon: Icons.bar_chart_rounded, label: 'Stats'),
FloatingNavItem(icon: Icons.home_rounded, label: 'Home'),
FloatingNavItem(icon: Icons.layers_rounded, label: 'Routines'),
FloatingNavItem(icon: Icons.history_rounded, label: 'History'),
FloatingNavItem(icon: Icons.bar_chart_rounded, label: 'Stats'),
];

void switchTab(int index) => setState(() => _currentIndex = index);

@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
backgroundColor: AppColors.background,
return FloatingNavBarScaffold(
scaffoldBackgroundColor: AppColors.background,
// App-specific colour overrides — all other values use the
// FloatingNavBarTheme defaults which adapt to ThemeData.colorScheme.
theme: FloatingNavBarTheme(
backgroundColor: AppColors.surface,
borderColor: AppColors.glassBorderStrong,
// Chip colours (replaces old pill API)
selectedChipColor: AppColors.glass3,
selectedChipBorderColor: AppColors.primary.withValues(alpha: 0.25),
selectedChipShadowColor: AppColors.primary.withValues(alpha: 0.15),
selectedContentColor: AppColors.textPrimary,
inactiveIconColor: AppColors.textMuted,
outerGlowColor: AppColors.primary.withValues(alpha: 0.08),
),
items: _navItems,
currentIndex: _currentIndex,
onTabChanged: switchTab,
body: IndexedStack(
index: _currentIndex,
children: const [
Expand All @@ -61,11 +77,6 @@ class _HomeScreenState extends State<HomeScreen> {
AnalyticsScreen(),
],
),
bottomNavigationBar: RFNavBar(
currentIndex: _currentIndex,
onTap: switchTab,
items: _navItems,
),
);
}

Expand Down
Loading