Clone and Push #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Clone and Push | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| job_id: | |
| description: "Import job id (for explorer status)" | |
| required: false | |
| git_url: | |
| description: "Git repository URL to clone" | |
| required: true | |
| org: | |
| description: "PowerSync org id" | |
| required: true | |
| repo: | |
| description: "PowerSync repo id" | |
| required: true | |
| jobs: | |
| clone-and-push: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install pnpm | |
| run: corepack enable && corepack prepare [email protected] --activate | |
| - name: Cache pnpm store | |
| id: pnpm-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pnpm-store | |
| key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| pnpm-store-${{ runner.os }}- | |
| - name: Install dependencies (workspace) | |
| run: pnpm install --no-frozen-lockfile | |
| - name: Build shared core | |
| run: pnpm --filter @powersync-community/powergit-core build | |
| - name: Build remote-helper | |
| run: pnpm --filter @powersync-community/powergit-remote-helper build | |
| - name: Add remote-helper to PATH | |
| run: | | |
| echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" | |
| echo "$PWD/packages/remote-helper/node_modules/.bin" >> "$GITHUB_PATH" | |
| ln -sf "$PWD/packages/remote-helper/dist/remote-helper/src/bin.js" "$PWD/packages/remote-helper/dist/remote-helper/src/git-remote-powergit" | |
| chmod +x "$PWD/packages/remote-helper/dist/remote-helper/src/git-remote-powergit" | |
| echo "$PWD/packages/remote-helper/dist/remote-helper/src" >> "$GITHUB_PATH" | |
| - name: Verify remote-helper is available | |
| run: | | |
| if ! command -v git-remote-powergit >/dev/null 2>&1; then | |
| echo "git-remote-powergit is missing from PATH. Contents of node_modules/.bin:" | |
| ls -l "$PWD/node_modules/.bin" || true | |
| ls -l "$PWD/packages/remote-helper/node_modules/.bin" || true | |
| ls -l "$PWD/packages/remote-helper/dist/remote-helper/src" || true | |
| exit 1 | |
| fi | |
| git remote -h | head -n 5 | |
| - name: Start Powergit daemon | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL || secrets.POWERSYNC_SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY || secrets.POWERSYNC_SUPABASE_ANON_KEY }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || secrets.POWERSYNC_SUPABASE_SERVICE_ROLE_KEY }} | |
| SUPABASE_JWT_SECRET: ${{ secrets.SUPABASE_JWT_SECRET || secrets.POWERSYNC_SUPABASE_JWT_SECRET }} | |
| POWERGIT_EMAIL: ${{ secrets.POWERGIT_EMAIL }} | |
| POWERGIT_PASSWORD: ${{ secrets.POWERGIT_PASSWORD }} | |
| POWERSYNC_DAEMON_PORT: 5030 | |
| POWERSYNC_SUPABASE_ONLY: "true" | |
| run: | | |
| # Fail fast on missing required secrets | |
| if [ -z "$SUPABASE_URL" ]; then echo "Missing SUPABASE_URL secret" && exit 1; fi | |
| if [ -z "$SUPABASE_ANON_KEY" ]; then echo "Missing SUPABASE_ANON_KEY secret" && exit 1; fi | |
| if [ -z "$POWERGIT_EMAIL" ]; then echo "Missing POWERGIT_EMAIL secret" && exit 1; fi | |
| if [ -z "$POWERGIT_PASSWORD" ]; then echo "Missing POWERGIT_PASSWORD secret" && exit 1; fi | |
| nohup pnpm --filter @powersync-community/powergit-daemon start -- --port ${POWERSYNC_DAEMON_PORT:-5030} > daemon.log 2>&1 & | |
| echo $! > daemon.pid | |
| for i in $(seq 1 30); do | |
| if curl -sf "http://127.0.0.1:${POWERSYNC_DAEMON_PORT:-5030}/health" >/dev/null; then | |
| echo "Daemon is healthy" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "Daemon did not become healthy. Logs:" && cat daemon.log && exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| # Verify auth status | |
| for i in $(seq 1 20); do | |
| STATUS_JSON=$(curl -sf "http://127.0.0.1:${POWERSYNC_DAEMON_PORT:-5030}/auth/status" || true) | |
| STATUS=$(echo "$STATUS_JSON" | jq -r '.status // empty') | |
| echo "Auth status attempt $i: ${STATUS_JSON:-<empty>}" | |
| if [ "$STATUS" = "ready" ]; then | |
| break | |
| fi | |
| if [ "$i" -eq 20 ]; then | |
| echo "Daemon did not authenticate. Full log:" && cat daemon.log && exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| - name: Record import job running | |
| if: ${{ github.event.inputs.job_id != '' }} | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL || secrets.POWERSYNC_SUPABASE_URL }} | |
| SUPABASE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || secrets.POWERSYNC_SUPABASE_SERVICE_ROLE_KEY || secrets.SUPABASE_ANON_KEY || secrets.POWERSYNC_SUPABASE_ANON_KEY }} | |
| JOB_ID: ${{ github.event.inputs.job_id }} | |
| ORG_ID: ${{ github.event.inputs.org }} | |
| REPO_ID: ${{ github.event.inputs.repo }} | |
| REPO_URL: ${{ github.event.inputs.git_url }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| node - <<'EOF' | |
| const { createClient } = require('@supabase/supabase-js') | |
| const required = ['SUPABASE_URL','SUPABASE_KEY','JOB_ID','ORG_ID','REPO_ID','REPO_URL'] | |
| for (const k of required) { | |
| if (!process.env[k] || !process.env[k].trim()) throw new Error(`Missing env ${k}`) | |
| } | |
| const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) | |
| const now = new Date().toISOString() | |
| const payload = { | |
| id: process.env.JOB_ID, | |
| org_id: process.env.ORG_ID, | |
| repo_id: process.env.REPO_ID, | |
| repo_url: process.env.REPO_URL, | |
| status: 'running', | |
| created_at: now, | |
| updated_at: now, | |
| source: 'actions', | |
| workflow_url: process.env.WORKFLOW_URL ?? null, | |
| } | |
| ;(async () => { | |
| const { error } = await supabase.from('import_jobs').upsert(payload, { onConflict: 'id' }) | |
| if (error) throw error | |
| })().catch((err) => { | |
| console.error('Failed to record import job running', err) | |
| process.exit(1) | |
| }) | |
| EOF | |
| - name: Clone target repo | |
| run: | | |
| git clone "${{ github.event.inputs.git_url }}" workdir | |
| cd workdir | |
| git rev-parse HEAD | |
| - name: Push | |
| env: | |
| POWERSYNC_ORG: ${{ github.event.inputs.org }} | |
| POWERSYNC_REPO: ${{ github.event.inputs.repo }} | |
| POWERSYNC_REPO_URL: ${{ github.event.inputs.git_url }} | |
| POWERSYNC_DAEMON_URL: http://127.0.0.1:5030 | |
| POWERSYNC_DAEMON_AUTOSTART: "false" | |
| run: | | |
| cd workdir | |
| git config user.email "[email protected]" | |
| git config user.name "GitHub Actions" | |
| git remote add powersync "powergit::/$POWERSYNC_ORG/$POWERSYNC_REPO" | |
| git push powersync --all | |
| git push powersync --tags || true | |
| - name: Show daemon repo summary | |
| run: | | |
| echo "Daemon /auth/status:" && curl -sf http://127.0.0.1:5030/auth/status || true | |
| echo "" | |
| echo "Daemon repo summary:" && curl -sf "http://127.0.0.1:5030/orgs/${{ github.event.inputs.org }}/repos/${{ github.event.inputs.repo }}/summary" || true | |
| echo "" | |
| echo "Daemon log (tail):" | |
| tail -n 200 daemon.log || true | |
| - name: Verify Supabase replication (fail if missing) | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| SUPABASE_EMAIL: ${{ secrets.POWERGIT_EMAIL }} | |
| SUPABASE_PASSWORD: ${{ secrets.POWERGIT_PASSWORD }} | |
| ORG_ID: ${{ github.event.inputs.org }} | |
| REPO_ID: ${{ github.event.inputs.repo }} | |
| run: | | |
| node - <<'EOF' | |
| const { createClient } = require('@supabase/supabase-js'); | |
| const required = ['SUPABASE_URL','SUPABASE_ANON_KEY','SUPABASE_EMAIL','SUPABASE_PASSWORD','ORG_ID','REPO_ID']; | |
| for (const key of required) { | |
| if (!process.env[key] || !process.env[key].trim()) { | |
| throw new Error(`Missing env ${key} for Supabase verification`); | |
| } | |
| } | |
| const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); | |
| const deadline = Date.now() + 120_000; // wait up to 2 minutes for writer to catch up | |
| const pollDelayMs = 5_000; | |
| async function main() { | |
| const { error: loginError } = await supabase.auth.signInWithPassword({ | |
| email: process.env.SUPABASE_EMAIL, | |
| password: process.env.SUPABASE_PASSWORD, | |
| }); | |
| if (loginError) throw new Error(`Supabase login failed: ${loginError.message}`); | |
| const tables = ['refs','commits','file_changes','objects']; | |
| let lastCounts = {}; | |
| while (Date.now() < deadline) { | |
| const counts = {}; | |
| for (const table of tables) { | |
| const { count, error } = await supabase | |
| .from(table) | |
| .select('id', { count: 'exact', head: true }) | |
| .eq('org_id', process.env.ORG_ID) | |
| .eq('repo_id', process.env.REPO_ID); | |
| if (error) throw new Error(`Count query failed for ${table}: ${error.message}`); | |
| counts[table] = count ?? 0; | |
| } | |
| lastCounts = counts; | |
| console.log('Supabase counts:', counts); | |
| if ((counts.refs ?? 0) > 0 && (counts.commits ?? 0) > 0) { | |
| return; | |
| } | |
| await new Promise((r) => setTimeout(r, pollDelayMs)); | |
| } | |
| throw new Error( | |
| `Supabase replication incomplete after waiting: refs=${lastCounts.refs ?? 0}, commits=${lastCounts.commits ?? 0}`, | |
| ); | |
| } | |
| main().catch((err) => { | |
| console.error(String(err?.message || err)); | |
| process.exit(1); | |
| }); | |
| EOF | |
| - name: Record import job result | |
| if: ${{ always() && github.event.inputs.job_id != '' }} | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL || secrets.POWERSYNC_SUPABASE_URL }} | |
| SUPABASE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || secrets.POWERSYNC_SUPABASE_SERVICE_ROLE_KEY || secrets.SUPABASE_ANON_KEY || secrets.POWERSYNC_SUPABASE_ANON_KEY }} | |
| JOB_ID: ${{ github.event.inputs.job_id }} | |
| WORKFLOW_STATUS: ${{ job.status }} | |
| run: | | |
| node - <<'EOF' | |
| const { createClient } = require('@supabase/supabase-js') | |
| const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) | |
| const now = new Date().toISOString() | |
| const status = process.env.WORKFLOW_STATUS === 'success' ? 'success' : 'error' | |
| const update = { | |
| status, | |
| updated_at: now, | |
| error: status === 'error' ? 'GitHub Actions import failed' : null, | |
| } | |
| ;(async () => { | |
| const { error } = await supabase.from('import_jobs').update(update).eq('id', process.env.JOB_ID) | |
| if (error) throw error | |
| })().catch((err) => { | |
| console.error('Failed to record import job result', err) | |
| process.exit(1) | |
| }) | |
| EOF |