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
38 changes: 20 additions & 18 deletions .github/workflows/build-push-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
SURGE_VERSION=$(curl -s https://api.github.com/repos/SurgeDM/Surge/releases/latest | jq -r .tag_name | sed 's/^v//')
echo "Latest Surge version: $SURGE_VERSION"
fi
echo "version=$SURGE_VERSION" >> $GITHUB_OUTPUT
echo "version=$SURGE_VERSION" >> "$GITHUB_OUTPUT"

- name: Wait for Release Assets
run: |
Expand All @@ -68,7 +68,7 @@ jobs:
exit 0
fi
echo "Attempt $i/$MAX_RETRIES: Release not ready yet, waiting ${RETRY_INTERVAL}s..."
sleep $RETRY_INTERVAL
sleep "$RETRY_INTERVAL"
done

echo "Timeout waiting for release assets after $((MAX_RETRIES * RETRY_INTERVAL / 60)) minutes."
Expand All @@ -85,7 +85,7 @@ jobs:
elif [[ "${{ inputs.tag_as_latest }}" == "true" ]]; then
ENABLE_LATEST=true
fi
echo "enable=$ENABLE_LATEST" >> $GITHUB_OUTPUT
echo "enable=$ENABLE_LATEST" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
Expand Down Expand Up @@ -125,18 +125,20 @@ jobs:

- name: Summary
run: |
echo "## Docker Image Built Successfully! 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Surge Version:** ${{ steps.surge-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Images pushed:**" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Pull the image:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.surge-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
{
echo "## Docker Image Built Successfully! 🚀"
echo ""
echo "**Surge Version:** ${{ steps.surge-version.outputs.version }}"
echo ""
echo "**Images pushed:**"
echo '```'
echo "${{ steps.meta.outputs.tags }}"
echo '```'
echo ""
echo "**Platforms:** linux/amd64, linux/arm64"
echo ""
echo "### Pull the image:"
echo '```bash'
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.surge-version.outputs.version }}"
Comment thread
bladeacer marked this conversation as resolved.
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
157 changes: 157 additions & 0 deletions .github/workflows/core-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- main

permissions:
actions: read
contents: write
pull-requests: write

Expand All @@ -24,6 +25,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
core: ${{ steps.filter.outputs.core }}
tui: ${{ steps.filter.outputs.tui }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
Expand All @@ -33,6 +35,10 @@ jobs:
core:
- '!extension/**'
- '**'
tui:
- 'internal/tui/**'
- 'go.mod'
- 'go.sum'

test:
name: Test and Check (${{ matrix.os }})
Expand Down Expand Up @@ -122,6 +128,157 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}

tui-perf:
name: TUI Performance Budget
needs: changes
if: needs.changes.outputs.tui == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25.0"
check-latest: false
- name: Enforce TUI frame budgets
env:
GOMAXPROCS: "2"
SURGE_PERF_BUDGET: "1"
run: |
set -o pipefail
go test ./internal/tui -run '^TestTUI.*RenderPerfBudget$' -count=3 -v | tee tui-perf.txt
- name: Report TUI performance values
if: always()
run: |
{
echo "### TUI performance budget"
echo
echo '```text'
if [ -f tui-perf.txt ]; then
cat tui-perf.txt
else
echo "No performance output was produced."
fi
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Record TUI performance metadata
if: always()
shell: bash
run: |
{
echo "sha=$GITHUB_SHA"
echo "run_id=$GITHUB_RUN_ID"
echo "run_number=$GITHUB_RUN_NUMBER"
echo "runner_os=$RUNNER_OS"
echo "runner_arch=$RUNNER_ARCH"
echo "gomaxprocs=${GOMAXPROCS:-2}"
date -u +"timestamp=%Y-%m-%dT%H:%M:%SZ"
} > tui-perf-metadata.txt
- name: Upload TUI performance history
if: always()
uses: actions/upload-artifact@v4
with:
name: tui-perf-${{ github.run_number }}
path: |
tui-perf.txt
tui-perf-metadata.txt
if-no-files-found: ignore
retention-days: 90

tui-perf-regression:
name: TUI Performance Regression
needs: [changes, tui-perf]
if: always() && needs.changes.outputs.tui == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Download current performance report
uses: actions/download-artifact@v4
with:
name: tui-perf-${{ github.run_number }}
path: current-perf

- name: Compare with previous successful baseline
env:
GH_TOKEN: ${{ github.token }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }}
shell: bash
run: |
set -euo pipefail

successful_runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/core-build.yml/runs" \
--method GET \
--field "branch=${BASE_BRANCH}" \
--field status=success \
--field per_page=100 \
--jq ".workflow_runs[] | select(.id != ${GITHUB_RUN_ID}) | [.id, .run_number] | @tsv" || true)"

if [ -z "$successful_runs" ]; then
printf '%s\n' "No previous successful TUI performance baseline found for ${BASE_BRANCH}; comparison skipped." > perf-comparison.txt
cat perf-comparison.txt
exit 0
fi

artifact_id=""
while IFS=$'\t' read -r candidate_run_id candidate_run_number; do
[ -n "$candidate_run_id" ] || continue
candidate_artifact_id="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${candidate_run_id}/artifacts" \
--jq ".artifacts[] | select(.expired == false and .name == \"tui-perf-${candidate_run_number}\") | .id" | head -n 1)"
if [ -n "$candidate_artifact_id" ]; then
artifact_id="$candidate_artifact_id"
break
fi
done <<< "$successful_runs"

if [ -z "$artifact_id" ]; then
printf '%s\n' "Previous successful runs have no unexpired TUI performance artifact; comparison skipped." > perf-comparison.txt
cat perf-comparison.txt
exit 0
fi

mkdir -p previous-perf
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}/zip" > previous-perf.zip
unzip -q previous-perf.zip -d previous-perf

CURRENT_REPORT="$(find current-perf -name tui-perf.txt -type f -print -quit)"
PREVIOUS_REPORT="$(find previous-perf -name tui-perf.txt -type f -print -quit)"
if [ -z "$CURRENT_REPORT" ] || [ -z "$PREVIOUS_REPORT" ]; then
printf '%s\n' "Performance report missing from current or previous artifact; comparison skipped." > perf-comparison.txt
cat perf-comparison.txt
exit 0
fi

python3 scripts/compare_tui_perf.py \
--current "$CURRENT_REPORT" \
--previous "$PREVIOUS_REPORT" \
--output perf-comparison.txt

- name: Report performance comparison
if: always()
run: |
{
echo "### TUI performance regression"
echo
echo '```text'
if [ -f perf-comparison.txt ]; then
cat perf-comparison.txt
else
echo "No comparison output was produced."
fi
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload performance comparison
if: always()
uses: actions/upload-artifact@v4
with:
name: tui-perf-comparison-${{ github.run_number }}
path: perf-comparison.txt
if-no-files-found: ignore
retention-days: 90

release:
name: Release
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/core-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ jobs:
run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run
- name: unicode lint
run: go test ./internal/lint/...
- name: TUI performance comparison tests
run: python3 -m unittest discover -s scripts -p 'test_*.py'
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y shellcheck
- name: ShellCheck scripts
shell: bash
run: find scripts -type f -name '*.sh' -print0 | xargs -0 -r shellcheck
- name: GitHub Actions lint
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
2 changes: 1 addition & 1 deletion .github/workflows/extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: |
VERSION=${GITHUB_REF_NAME#ext-v}
echo "Setting version to $VERSION"
npm version $VERSION --no-git-tag-version
npm version "$VERSION" --no-git-tag-version
- name: Build and Package
run: |
npm run zip -- -b ${{ matrix.browser }}
Expand Down
87 changes: 57 additions & 30 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,76 @@
# Contributing

Thanks for checking out Surge. We are very open to contributions and happy to review PRs.
Thanks for contributing to Surge. Start with the [Development Guide](docs/DEVELOPMENT.md) for prerequisites and the complete local workflow.

This is intentionally short. If you see something that can be better, open a PR.
## Quick start

## Quick Codebase Map
From the repository root:

- `cmd/`: CLI commands and startup wiring (`surge get`, `surge server`, etc.).
- `internal/core/`: service layer (`LocalDownloadService`) that orchestrates add/pause/resume/delete/list.
- `internal/download/`: high-level download flow (`RunDownload`) and worker-pool lifecycle.
- `internal/engine/`: low-level engine code.
- `internal/engine/probe.go`: probe logic (range support, metadata, mirror probing).
- `internal/engine/concurrent/`: concurrent HTTP downloader and worker/retry/failover logic.
- `internal/engine/single/`: single-connection HTTP downloader fallback.
- `internal/engine/state/`: Gob-backed file persistence for paused/history downloads.
- `internal/tui/`: terminal UI models, update loop, views.
- `internal/testutil/`: mock HTTP servers and test helpers.
```bash
go mod download
go test ./...
go test -race ./internal/...
```

The core Go workflow uses the dependencies pinned in `go.mod` and `go.sum`. The Python performance scripts require Python 3.8+ but only use the standard library, so no pip install is needed. Browser-extension work additionally requires Node.js 22+ and `npm ci` in `extension/`; ShellCheck and actionlint are optional locally and are enforced by CI.

If you are looking for networking behavior, start here:
For local configuration isolation while running the TUI or server:

```bash
XDG_CONFIG_HOME="$(mktemp -d)" XDG_CACHE_HOME="$(mktemp -d)" go run .
```

1. `internal/engine/probe.go`
2. `internal/engine/concurrent/`
3. `internal/engine/single/`
## Codebase map

## Run Tests
- `cmd/`: Cobra commands, startup wiring, and CLI/server entry points.
- `internal/orchestrator/`: lifecycle management, enqueueing, pause/resume, and event coordination.
- `internal/scheduler/`: queued/active download scheduling, rate-limit pools, and shutdown behavior.
- `internal/strategy/concurrent/`: ranged, mirrored, retried, hedged, and health-monitored downloads.
- `internal/strategy/single/`: single-connection fallback downloads and throttled streaming.
- `internal/probe/`: server capability and metadata probing.
- `internal/transport/`: network pools, host penalties, and byte rate limiters.
- `internal/progress/`: live download state, chunk maps, and progress aggregation.
- `internal/store/`: persisted download and resume state.
- `internal/tui/`: Bubble Tea model/update loop, dashboard panes, modal components, and render tests.
- `internal/testutil/`: mock servers, temporary directories, and test helpers.
- `extension/`: WXT/Solid browser extension source and tests.

From repo root:
## Focused checks

Use focused package tests while iterating:

```bash
go test ./...
go test ./internal/tui ./internal/tui/components -count=1
go test ./internal/strategy/concurrent -count=1
go test ./internal/strategy/single -count=1
go test ./internal/transport -run 'RateLimiter|HostRateLimiter' -count=1
```

For TUI rendering changes, also run the opt-in budgets and rendering benchmarks:

```bash
GOMAXPROCS=2 SURGE_PERF_BUDGET=1 \
go test ./internal/tui -run '^TestTUI.*RenderPerfBudget$' -count=3 -v
go test ./internal/tui -run '^$' \
-bench '^BenchmarkCPU_(FullView_Old|FullView_New|DashboardPanes_Cached)$' \
-benchmem -count=3 -benchtime=500ms
```

Useful focused runs:
For workflow or shell changes:

```bash
go test ./internal/engine/concurrent -run TestConcurrentDownloader_SwitchOn429 -count=1
go test ./internal/download -run TestIntegration_PauseResume -count=1
go test ./internal/tui -count=1
find scripts -type f -name '*.sh' -print0 | xargs -0 -r shellcheck
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
```

## PR Expectations
## Pull requests

- Keep PRs focused and readable.
- Add or update tests for behavior changes.
- Run `go test ./...` before opening/updating the PR.
- If behavior or CLI usage changes, update docs (`README.md` or `docs/`).
- Keep each PR focused and explain the user-visible motivation.
- Add regression tests for behavior changes and golden tests for terminal rendering changes.
- Preserve existing platform behavior; avoid assuming Linux unless the code path is platform-specific.
- Run `gofmt` on changed Go files.
- Run `go test ./...` and, for core changes, `go test -race ./internal/...`.
- Update `README.md`, `docs/`, or CLI help when setup, behavior, or user-facing commands change.
- Do not commit generated binaries, profiles, perf reports, temporary config directories, or downloaded artifacts.

That is it. If you are unsure about approach, open a draft PR early and we can iterate on it together.
CI runs the full race-tested Go suite across the supported operating systems, TUI performance budgets when relevant files change, ShellCheck, actionlint, and extension checks when extension files change.
Loading
Loading