Skip to content
Open
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
81 changes: 81 additions & 0 deletions .github/workflows/sync-upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Sync Upstream and Release

on:
schedule:
# Check for upstream changes daily at 00:00 UTC
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
force_release:
description: 'Force a release even if no upstream changes'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'

env:
UPSTREAM_REPO: https://github.com/router-for-me/EasyCLI.git
UPSTREAM_BRANCH: main

jobs:
sync:
name: Sync Upstream
runs-on: ubuntu-latest
steps:
Comment on lines +18 to +26
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This workflow pushes commits and tags back to the repository using GITHUB_TOKEN, but it doesn't declare explicit token permissions. To make scheduled/manual runs reliable across org/repo settings, add permissions: contents: write (and any other required scopes) at the workflow or job level.

Copilot uses AI. Check for mistakes.
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Add upstream remote
run: |
git remote add upstream ${{ env.UPSTREAM_REPO }} || true
git fetch upstream ${{ env.UPSTREAM_BRANCH }}

- name: Check for upstream changes
id: check
run: |
LOCAL_SHA=$(git rev-parse HEAD)
UPSTREAM_SHA=$(git rev-parse upstream/${{ env.UPSTREAM_BRANCH }})

if [ "$LOCAL_SHA" = "$UPSTREAM_SHA" ] && [ "${{ github.event.inputs.force_release }}" != "true" ]; then
echo "No upstream changes detected."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Upstream changes detected or force release requested."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
Comment on lines +46 to +55
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The upstream-change detection compares HEAD vs upstream/main SHA equality. If this repo intentionally carries additional local commits on top of upstream (common for forks), the SHAs will always differ even when upstream has no new commits, causing unnecessary merges/tags. Consider checking whether upstream is ahead of local instead (e.g., count commits in HEAD..upstream/main) and only sync when that count is non-zero (unless force_release is true).

Copilot uses AI. Check for mistakes.

- name: Merge upstream changes
if: steps.check.outputs.has_changes == 'true'
run: |
git merge upstream/${{ env.UPSTREAM_BRANCH }} --no-edit --allow-unrelated-histories || {
echo "::warning::Merge conflict detected. Resolving by keeping our changes."
git checkout --ours .
git add .
git commit -m "Merge upstream (conflicts resolved by keeping local)"
Comment on lines +61 to +64
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Auto-resolving merge conflicts with git checkout --ours . silently discards upstream changes in conflicting files, which defeats the purpose of syncing (and can miss important fixes). A safer approach is to fail the job on conflicts and either open an issue/PR for manual resolution or push the conflicted merge to a separate branch for review.

Suggested change
echo "::warning::Merge conflict detected. Resolving by keeping our changes."
git checkout --ours .
git add .
git commit -m "Merge upstream (conflicts resolved by keeping local)"
echo "::error::Merge conflict detected while syncing with upstream. Please resolve manually."
exit 1

Copilot uses AI. Check for mistakes.
}

- name: Push merged changes
if: steps.check.outputs.has_changes == 'true'
run: |
git push origin HEAD

Comment on lines +67 to +71
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Pushing merged changes directly to origin HEAD on a schedule can fail on protected default branches and makes it easy to publish unreviewed upstream merges. Consider pushing the merge result to a dedicated bot branch and opening/updating a PR instead, then tagging only after that PR is merged.

Copilot uses AI. Check for mistakes.
- name: Create release tag (triggers build workflow)
if: steps.check.outputs.has_changes == 'true'
run: |
DATE=$(date +%Y%m%d)
SHORT_SHA=$(git rev-parse --short HEAD)
TAG="v${DATE}-sync-${SHORT_SHA}"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The tag name v${DATE}-sync-${SHORT_SHA} can collide if the workflow is re-run for the same commit/day (e.g., manual re-run after a transient failure), causing git tag/git push to fail. Consider checking for an existing tag before creating it, or including ${{ github.run_id }}/${{ github.run_number }} in the tag to guarantee uniqueness.

Suggested change
TAG="v${DATE}-sync-${SHORT_SHA}"
TAG="v${DATE}-sync-${SHORT_SHA}-${GITHUB_RUN_ID}"

Copilot uses AI. Check for mistakes.
echo "Creating tag: ${TAG}"
git tag "$TAG"
git push origin "$TAG"
echo "Tag pushed — the 'Release on Tag' workflow will handle the build."
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@ dist-web/
src-tauri/logs/
.serena/
AGENTS.md
CLAUDE.md
CLAUDE.md

# codex: local build artifacts
.vs/
**/.vs/
[Bb]in/
[Oo]bj/
**/[Bb]in/
**/[Oo]bj/
node_modules/
**/node_modules/
# codex: end local build artifacts
15 changes: 11 additions & 4 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,15 @@ fn generate_random_password() -> String {
.collect()
}

fn get_or_generate_secret_key(conf: &serde_yaml::Value) -> String {
conf.get("remote-management")
.and_then(|v| v.get("secret-key"))
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.map(|s| s.to_string())
.unwrap_or_else(generate_random_password)
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Add unit tests for get_or_generate_secret_key to cover: (1) missing remote-management, (2) missing/blank secret-key, and (3) existing non-empty secret-key being reused. There are already Rust unit tests in this file, so adding coverage here should be straightforward and prevents regressions in startup auth behavior.

Suggested change
#[cfg(test)]
mod auth_tests {
use super::get_or_generate_secret_key;
#[test]
fn missing_remote_management_generates_secret_key() {
// No `remote-management` section -> should generate a new key
let conf: serde_yaml::Value = serde_yaml::from_str(
r#"
auth-dir: ./auth
"#,
)
.expect("failed to parse yaml");
let key = get_or_generate_secret_key(&conf);
assert!(!key.trim().is_empty(), "generated key should not be blank");
assert_eq!(key.len(), 32, "generated key should be 32 characters long");
}
#[test]
fn missing_or_blank_secret_key_generates_secret_key() {
// `remote-management` exists but `secret-key` is missing
let conf_missing: serde_yaml::Value = serde_yaml::from_str(
r#"
remote-management: {}
"#,
)
.expect("failed to parse yaml");
let key_missing = get_or_generate_secret_key(&conf_missing);
assert!(!key_missing.trim().is_empty());
assert_eq!(key_missing.len(), 32);
// `remote-management.secret-key` is present but blank/whitespace
let conf_blank: serde_yaml::Value = serde_yaml::from_str(
r#"
remote-management:
secret-key: " "
"#,
)
.expect("failed to parse yaml");
let key_blank = get_or_generate_secret_key(&conf_blank);
assert!(!key_blank.trim().is_empty());
assert_eq!(key_blank.len(), 32);
}
#[test]
fn existing_non_empty_secret_key_is_reused() {
let conf: serde_yaml::Value = serde_yaml::from_str(
r#"
remote-management:
secret-key: "my-fixed-secret"
"#,
)
.expect("failed to parse yaml");
let key = get_or_generate_secret_key(&conf);
assert_eq!(key, "my-fixed-secret");
}
}

Copilot uses AI. Check for mistakes.
fn start_monitor(app: tauri::AppHandle) {
let proc_ref = Arc::clone(&PROCESS);
thread::spawn(move || {
Expand Down Expand Up @@ -1128,8 +1137,7 @@ fn start_cliproxyapi(app: tauri::AppHandle) -> Result<serde_json::Value, String>
eprintln!("[PORT_CLEANUP] Warning: {}", e);
}

// Generate random password for local mode
let password = generate_random_password();
let password = get_or_generate_secret_key(&conf);

// Store the password for keep-alive authentication
*CLI_PROXY_PASSWORD.lock() = Some(password.clone());
Comment on lines 1139 to 1143
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

password can now be sourced from a persisted remote-management.secret-key, but the function later logs the full command-line args including --password (which will leak the management secret into logs/console). Please redact the password in logs (or remove it from the logged args) to avoid exposing a long-lived credential.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -1258,8 +1266,7 @@ fn restart_cliproxyapi(app: tauri::AppHandle) -> Result<(), String> {
eprintln!("[PORT_CLEANUP] Warning: {}", e);
}

// Generate random password for local mode
let password = generate_random_password();
let password = get_or_generate_secret_key(&conf);

// Store the password for keep-alive authentication
*CLI_PROXY_PASSWORD.lock() = Some(password.clone());
Expand Down
Loading