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
4 changes: 2 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ Git-Same is a Rust CLI + TUI tool that discovers GitHub org/repo structures and
### Core modules

- **`auth/`** — GitHub CLI (`gh`) authentication only (`gh auth token`), with SSH clone support
- **`config/`** — TOML config parser. Default location: `~/.config/git-same/config.toml`. Sections: `[clone]`, `[filters]`, `[[providers]]`
- **`config/`** — TOML config parser. Default: `~/.config/git-same/config.toml`. Top-level keys: `workspaces`, `default_workspace`, plus `[clone]` and `[filters]` sections
- **`discovery/`** — `DiscoveryOrchestrator` coordinates repo discovery via providers, applies filters, builds `ActionPlan` (what to clone vs sync)
- **`operations/clone/`** — `CloneManager` handles concurrent cloning (configurable 1–32, default 4)
- **`operations/sync/`** — `SyncManager` handles fetch/pull with concurrency. Detects repos with uncommitted changes and optionally skips them
- **`provider/`** — Trait-based provider abstraction (`Provider` trait in `traits.rs`). GitHub implementation in `github/client.rs` with pagination. Mock provider in `mock.rs` for testing
- **`git/`** — `GitOperations` trait (`traits.rs`) with `ShellGit` implementation (`shell.rs`) that shells out to `git` commands
- **`cache/`** — `DiscoveryCache` with TTL-based validity at `~/.cache/git-same/`
- **`cache/`** — `DiscoveryCache` with TTL-based validity, persisted per workspace at `<workspace-root>/.git-same/cache.json`
- **`errors/`** — Custom error hierarchy: `AppError`, `GitError`, `ProviderError` with `suggested_action()` methods
- **`output/`** — Verbosity levels and `indicatif` progress bars (`CloneProgressBar`, `SyncProgressBar`, `DiscoveryProgressBar`)
- **`types/repo.rs`** — Core data types: `Repo`, `Org`, `ActionPlan`, `OpResult`, `OpSummary`
Expand Down
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
115 changes: 110 additions & 5 deletions .github/workflows/S1-Test-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

permissions:
contents: read

jobs:
test:
name: Test (${{ matrix.os }})
Expand All @@ -22,6 +25,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -32,13 +37,13 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all -- --check
run: cargo +${{ matrix.rust }} fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
run: cargo +${{ matrix.rust }} clippy --all-targets --all-features -- -D warnings

- name: Run tests
run: cargo test --all-features
run: cargo +${{ matrix.rust }} test --all-features

build:
name: Build (${{ matrix.target }})
Expand All @@ -63,6 +68,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -76,15 +83,20 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Build release
run: cargo build --release --target ${{ matrix.target }}
run: cargo +stable build --release --target ${{ matrix.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'aarch64-linux-gnu-gcc' || '' }}

coverage:
name: Code Coverage
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -97,18 +109,111 @@ jobs:
tool: cargo-tarpaulin

- name: Generate coverage
run: cargo tarpaulin --all-features --workspace --timeout 120 --out xml
run: cargo +stable tarpaulin --all-features --workspace --timeout 120 --out xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
use_oidc: true
fail_ci_if_error: false

alias-drift-check:
name: Alias Drift Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Verify alias manifest and consumers
run: |
set -e

# Manifest exists
if [ ! -f toolkit/packaging/binary-aliases.txt ]; then
echo "ERROR: toolkit/packaging/binary-aliases.txt not found"; exit 1
fi

# Only 1 [[bin]] in Cargo.toml
BIN_COUNT=$(grep -c '^[[:space:]]*\[\[bin\]\]' Cargo.toml || true)
if [ "$BIN_COUNT" -ne 1 ]; then
echo "ERROR: Cargo.toml has $BIN_COUNT [[bin]] entries, expected 1"; exit 1
Comment on lines +130 to +140
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the [[bin]] count check resilient under set -e.

With set -e, grep -c can exit early in the zero-match case before your custom error message is emitted, and the pattern misses leading whitespace before [[bin]].

Suggested fix
-          BIN_COUNT=$(grep -c '^\[\[bin\]\]' Cargo.toml)
+          BIN_COUNT=$(grep -c '^[[:space:]]*\[\[bin\]\]' Cargo.toml || true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/S1-Test-CI.yml around lines 130 - 140, Change how
BIN_COUNT is computed so the grep invocation is resilient to set -e and matches
leading whitespace before [[bin]]; specifically update the BIN_COUNT assignment
that uses grep -c (the current pattern '^\[\[bin\]\]') to use a regex that
permits leading whitespace (e.g., '^[[:space:]]*\[\[bin\]\]') and ensure the
grep command cannot cause the script to exit under set -e by allowing a non-zero
grep exit to be ignored (for example via "|| true").

fi

# Primary matches Cargo.toml bin name
MANIFEST_PRIMARY=$(head -n1 toolkit/packaging/binary-aliases.txt)
CARGO_BIN=$(awk '
/^\[\[bin\]\]/ { in_bin=1; next }
in_bin && /^\[/ { exit }
in_bin && $1 == "name" {
line = $0
sub(/^[^"]*"/, "", line)
sub(/".*$/, "", line)
if (line != "") { print line; exit }
}
' Cargo.toml)
if [ -z "$CARGO_BIN" ]; then
echo "ERROR: Could not resolve bin name from Cargo.toml"; exit 1
fi
if [ "$MANIFEST_PRIMARY" != "$CARGO_BIN" ]; then
echo "ERROR: Primary mismatch: manifest=$MANIFEST_PRIMARY cargo=$CARGO_BIN"; exit 1
fi

# Homebrew workflow references all aliases
while IFS= read -r alias; do
[ -z "$alias" ] && continue
if ! grep -Fq -- "$alias" .github/workflows/S3-Publish-Homebrew.yml; then
echo "ERROR: '$alias' missing from S3-Publish-Homebrew.yml"; exit 1
fi
done < toolkit/packaging/binary-aliases.txt

# Conductor script paths exist with exact casing (important on Linux)
[ -f toolkit/conductor/run.sh ] || { echo "ERROR: toolkit/conductor/run.sh not found"; exit 1; }
[ -f toolkit/conductor/archive.sh ] || { echo "ERROR: toolkit/conductor/archive.sh not found"; exit 1; }

# Scripts reference the manifest
grep -q 'binary-aliases.txt' toolkit/conductor/run.sh || { echo "ERROR: run.sh missing manifest ref"; exit 1; }
grep -q 'binary-aliases.txt' toolkit/conductor/archive.sh || { echo "ERROR: archive.sh missing manifest ref"; exit 1; }

echo "All alias drift checks passed."

workflow-secret-safety:
name: Workflow Secret Safety
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Block known secret-leak patterns in workflows
run: |
set -euo pipefail

# Prevent credentials embedded in URLs (high leak risk in logs/config).
if grep -RInE 'https://[^/@[:space:]]+@github[.]com' .github/workflows; then
echo "ERROR: Credential-in-URL pattern found in workflow files."
exit 1
fi

# Prevent direct printing of expressions that evaluate from secrets.
if grep -RInE 'echo[[:space:]].*\$\{\{[[:space:]]*secrets\.' .github/workflows; then
echo "ERROR: Direct echo of secrets expression found in workflow files."
exit 1
fi

# Prevent shell xtrace in workflow scripts.
if grep -RInE '(^|[[:space:];])set[[:space:]]+-[a-wyzA-WYZ]*x|bash[[:space:]]+-[a-wyzA-WYZ]*x' .github/workflows; then
echo "ERROR: Shell xtrace detected in workflow files."
exit 1
fi

audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
114 changes: 108 additions & 6 deletions .github/workflows/S2-Release-GitHub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -35,19 +37,24 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all -- --check
run: cargo +${{ matrix.rust }} fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
run: cargo +${{ matrix.rust }} clippy --all-targets --all-features -- -D warnings

- name: Run tests
run: cargo test --all-features
run: cargo +${{ matrix.rust }} test --all-features

coverage:
name: Code Coverage
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -60,25 +67,118 @@ jobs:
tool: cargo-tarpaulin

- name: Generate coverage
run: cargo tarpaulin --all-features --workspace --timeout 120 --out xml
run: cargo +stable tarpaulin --all-features --workspace --timeout 120 --out xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
use_oidc: true
fail_ci_if_error: false

alias-drift-check:
name: Alias Drift Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Verify alias manifest and consumers
run: |
set -e

# Manifest exists
if [ ! -f toolkit/packaging/binary-aliases.txt ]; then
echo "ERROR: toolkit/packaging/binary-aliases.txt not found"; exit 1
fi

# Only 1 [[bin]] in Cargo.toml
BIN_COUNT=$(grep -c '^[[:space:]]*\[\[bin\]\]' Cargo.toml || true)
if [ "$BIN_COUNT" -ne 1 ]; then
echo "ERROR: Cargo.toml has $BIN_COUNT [[bin]] entries, expected 1"; exit 1
fi

# Primary matches Cargo.toml bin name
MANIFEST_PRIMARY=$(head -n1 toolkit/packaging/binary-aliases.txt)
CARGO_BIN=$(awk '
/^\[\[bin\]\]/ { in_bin=1; next }
in_bin && /^\[/ { exit }
in_bin && $1 == "name" {
line = $0
sub(/^[^"]*"/, "", line)
sub(/".*$/, "", line)
if (line != "") { print line; exit }
}
' Cargo.toml)
if [ -z "$CARGO_BIN" ]; then
echo "ERROR: Could not resolve bin name from Cargo.toml"; exit 1
fi
if [ "$MANIFEST_PRIMARY" != "$CARGO_BIN" ]; then
echo "ERROR: Primary mismatch: manifest=$MANIFEST_PRIMARY cargo=$CARGO_BIN"; exit 1
fi

# Homebrew workflow references all aliases
while IFS= read -r alias; do
[ -z "$alias" ] && continue
if ! grep -Fq -- "$alias" .github/workflows/S3-Publish-Homebrew.yml; then
echo "ERROR: '$alias' missing from S3-Publish-Homebrew.yml"; exit 1
fi
done < toolkit/packaging/binary-aliases.txt

# Conductor script paths exist with exact casing (important on Linux)
[ -f toolkit/conductor/run.sh ] || { echo "ERROR: toolkit/conductor/run.sh not found"; exit 1; }
[ -f toolkit/conductor/archive.sh ] || { echo "ERROR: toolkit/conductor/archive.sh not found"; exit 1; }

# Scripts reference the manifest
grep -q 'binary-aliases.txt' toolkit/conductor/run.sh || { echo "ERROR: run.sh missing manifest ref"; exit 1; }
grep -q 'binary-aliases.txt' toolkit/conductor/archive.sh || { echo "ERROR: archive.sh missing manifest ref"; exit 1; }

echo "All alias drift checks passed."

audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}

workflow-secret-safety:
name: Workflow Secret Safety
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Block known secret-leak patterns in workflows
run: |
set -euo pipefail

# Prevent credentials embedded in URLs (high leak risk in logs/config).
if grep -RInE 'https://[^/@[:space:]]+@github[.]com' .github/workflows; then
echo "ERROR: Credential-in-URL pattern found in workflow files."
exit 1
fi

# Prevent direct printing of expressions that evaluate from secrets.
if grep -RInE 'echo[[:space:]].*\$\{\{[[:space:]]*secrets\.' .github/workflows; then
echo "ERROR: Direct echo of secrets expression found in workflow files."
exit 1
fi

# Prevent shell xtrace in workflow scripts.
if grep -RInE '(^|[[:space:];])set[[:space:]]+-[a-wyzA-WYZ]*x|bash[[:space:]]+-[a-wyzA-WYZ]*x' .github/workflows; then
echo "ERROR: Shell xtrace detected in workflow files."
exit 1
fi

build-release-assets:
name: Build Release Asset (${{ matrix.target }})
needs: [test, coverage, audit]
needs: [test, coverage, alias-drift-check, audit, workflow-secret-safety]
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand Down Expand Up @@ -110,6 +210,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand All @@ -123,7 +225,7 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Build
run: cargo build --release --target ${{ matrix.target }}
run: cargo +stable build --release --target ${{ matrix.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'aarch64-linux-gnu-gcc' || '' }}

Expand Down
Loading