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
Binary file modified .DS_Store
Binary file not shown.
11 changes: 6 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ jobs:
# Checkout
# ------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

# ------------------------------------------------------
# Setup Go
# ------------------------------------------------------
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.24"

# ------------------------------------------------------
# Setup Node
# ------------------------------------------------------
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"

Expand Down Expand Up @@ -127,9 +127,10 @@ jobs:
- name: Create Unified Release
env:
GH_TOKEN: ${{ github.token }}
REF_NAME: ${{ github.ref_name }}
run: |
gh release create \
"${{ github.ref_name }}" \
"$REF_NAME" \
percy-linux.zip percy-mac.zip percy-windows.zip \
--title "Percy Local Manager ${{ github.ref_name }}" \
--title "Percy Local Manager $REF_NAME" \
--notes "Percy Local Manager for Linux, macOS, and Windows."
166 changes: 145 additions & 21 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Percy Local Manager — Tests & Release

on:
push:
branches: [main]
pull_request:
branches: [main]
pull_request:
branches:
- main

jobs:
# ============================================================
Expand All @@ -18,10 +18,10 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.21'

Expand All @@ -44,7 +44,7 @@ jobs:
done

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
Expand Down Expand Up @@ -99,7 +99,7 @@ jobs:

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-report
path: tests/playwright-report/
Expand All @@ -112,24 +112,148 @@ jobs:
kill $VITE_PID || true

# ============================================================
# Job 2: BrowserStack Automate — API + E2E on Mac & Windows
# Runs after local tests pass. Skipped if secrets not set.
# Job 2: BrowserStack Automate — OS X only (tests/browserstack.macos.yml)
# Job 3: BrowserStack Automate — Windows only (tests/browserstack.windows.yml)
#
# The Go backend is shared state: ONE snapshot queue and ONE build slot (a
# second finalize returns 409), and the BrowserStack SDK runs every platform
# of one invocation at the same time. So each platform gets its own job: own
# runner, own backend + Vite server, own single-platform config. The two jobs
# run simultaneously, which keeps the total build time down.
# ============================================================
test-browserstack:
name: BrowserStack Automate (Mac + Windows)
needs: test
test-browserstack-macos:
name: BrowserStack Automate (OS X)
runs-on: ubuntu-latest
permissions:
contents: read
# Only run on push to main (not on PRs) — requires BrowserStack secrets to be set
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

# Both platform jobs derive the SAME build number from the workflow run
# (run_number + run_attempt), so OS X and Windows report into ONE build.
- name: Generate build number and write it to browserstack.macos.yml
working-directory: tests
env:
RUN_NUMBER: ${{ github.run_number }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
BUILD_NUMBER="${RUN_NUMBER}.${RUN_ATTEMPT}"
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
sed -i "s|^buildIdentifier:.*|buildIdentifier: '#${BUILD_NUMBER}'|" browserstack.macos.yml
grep -n '^buildName\|^buildIdentifier' browserstack.macos.yml

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.21'

- name: Build Go backend
working-directory: go-backend
run: go build -o server ./cmd/server

- name: Start Go backend
working-directory: go-backend
run: |
./server &
echo "SERVER_PID=$!" >> $GITHUB_ENV
for i in $(seq 1 20); do
if curl -sf http://localhost:4321/health > /dev/null; then
echo "Backend is up"
break
fi
echo "Waiting for backend... ($i)"
sleep 1
done

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests/package-lock.json

- name: Install test dependencies
working-directory: tests
run: npm ci

- name: Install extension dependencies and start Vite dev server
working-directory: percy-local-manager-extension
run: |
npm ci --legacy-peer-deps
npm run dev &
echo "VITE_PID=$!" >> $GITHUB_ENV
for i in $(seq 1 20); do
if curl -sf http://localhost:5173 > /dev/null; then
echo "Vite dev server is up"
break
fi
echo "Waiting for Vite... ($i)"
sleep 1
done

- name: Run all tests on BrowserStack — OS X (API + E2E + Extension UI)
working-directory: tests
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BACKEND_URL: http://localhost:4321
EXTENSION_DEV_URL: http://localhost:5173
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_READ_TOKEN: ${{ secrets.PERCY_READ_TOKEN }}
run: npx browserstack-node-sdk playwright test --workers=1 --browserstack.config=browserstack.macos.yml

- name: Fetch BrowserStack build public URL
if: always()
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: |
BUILD_URL=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
"https://api.browserstack.com/automate/builds.json?limit=1" \
| python3 -c "import sys,json; b=json.load(sys.stdin)[0]['automation_build']; print(b.get('public_url') or 'https://automate.browserstack.com/builds/' + b['hashed_id'])")
echo "BrowserStack Build URL: $BUILD_URL"
echo "BROWSERSTACK_BUILD_URL=$BUILD_URL" >> $GITHUB_ENV

- name: Upload BrowserStack Playwright report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: browserstack-playwright-report-macos
path: tests/playwright-report/
retention-days: 14

- name: Stop Go backend and Vite dev server
if: always()
run: |
kill $SERVER_PID || true
kill $VITE_PID || true

test-browserstack-windows:
name: BrowserStack Automate (Windows)
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

# Same derivation as the OS X job, so both land in the same build.
- name: Generate build number and write it to browserstack.windows.yml
working-directory: tests
env:
RUN_NUMBER: ${{ github.run_number }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
BUILD_NUMBER="${RUN_NUMBER}.${RUN_ATTEMPT}"
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
sed -i "s|^buildIdentifier:.*|buildIdentifier: '#${BUILD_NUMBER}'|" browserstack.windows.yml
grep -n '^buildName\|^buildIdentifier' browserstack.windows.yml

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.21'

Expand All @@ -152,7 +276,7 @@ jobs:
done

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
Expand All @@ -177,7 +301,7 @@ jobs:
sleep 1
done

- name: Run all tests on BrowserStack (API + E2E + Extension UI)
- name: Run all tests on BrowserStack — Windows (API + E2E + Extension UI)
working-directory: tests
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
Expand All @@ -186,7 +310,7 @@ jobs:
EXTENSION_DEV_URL: http://localhost:5173
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_READ_TOKEN: ${{ secrets.PERCY_READ_TOKEN }}
run: npm run test:bstack
run: npx browserstack-node-sdk playwright test --workers=1 --browserstack.config=browserstack.windows.yml

- name: Fetch BrowserStack build public URL
if: always()
Expand All @@ -202,9 +326,9 @@ jobs:

- name: Upload BrowserStack Playwright report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: browserstack-playwright-report
name: browserstack-playwright-report-windows
path: tests/playwright-report/
retention-days: 14

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ tests/node_modules/
tests/test-results/
tests/playwright-report/
tests/log/

# Test Companion onboarding (local, per-machine)
.test-companion/
24 changes: 16 additions & 8 deletions go-backend/internal/api/handlers/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package handlers

import (
"encoding/json"
"errors"
"net/http"

"github.com/jostan30/Percy_chrome_ext/go-backend/internal/app"
"github.com/jostan30/Percy_chrome_ext/go-backend/internal/httpx"
"github.com/jostan30/Percy_chrome_ext/go-backend/internal/service"
)

type BuildHandler struct {
Expand All @@ -32,17 +34,23 @@ func (h *BuildHandler) Finalize(w http.ResponseWriter, r *http.Request) {
return
}

result , err := h.app.BuildService.Finalize(req.Token)
if err != nil {
httpx.WriteJSON(w, http.StatusInternalServerError ,map [string] string {
if err := h.app.BuildService.StartFinalize(req.Token); err != nil {
code := http.StatusInternalServerError
if errors.Is(err, service.ErrBuildRunning) {
code = http.StatusConflict
}
httpx.WriteJSON(w, code, map[string]string{
"error": err.Error(),
})
return
}

httpx.WriteJSON(w, http.StatusOK, map[string] string {
"message":"Percy started successfully",
"buildId": result.BuildID,
"buildUrl": result.BuildURL,
})
httpx.WriteJSON(w, http.StatusAccepted, map[string]string{
"status": "running",
})
}

// Status returns the current build progress and logs.
func (h *BuildHandler) Status(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, h.app.BuildService.Status())
}
9 changes: 8 additions & 1 deletion go-backend/internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"net/http"
"time"

"github.com/jostan30/Percy_chrome_ext/go-backend/internal/api/handlers"
"github.com/jostan30/Percy_chrome_ext/go-backend/internal/middleware"
Expand Down Expand Up @@ -70,6 +71,7 @@ func (s *Server) routes() {
})

s.mux.HandleFunc("/build/finalize", buildHandler.Finalize)
s.mux.HandleFunc("/build/status", buildHandler.Status)

s.mux.HandleFunc("/library/token" ,libraryHandler.SetToken)
s.mux.HandleFunc("/library/search", libraryHandler.Search)
Expand All @@ -85,6 +87,11 @@ func (s *Server) Start() error {
middleware.Logger,
middleware.CORS,
)
return http.ListenAndServe(s.addr ,handler)
srv := &http.Server{
Addr: s.addr,
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
}
return srv.ListenAndServe()
}

1 change: 1 addition & 0 deletions go-backend/internal/middleware/cors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "net/http"
func CORS(next http.Handler)http.Handler {
return http.HandlerFunc(func (w http.ResponseWriter ,r *http.Request){

// nosemgrep: local-only backend (localhost:4321) called by the browser extension; wildcard origin is intentional
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS")
Expand Down
Loading
Loading