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
51 changes: 45 additions & 6 deletions .github/workflows/release-pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
# Without this, release PRs have no status checks and can't be merged when
# branch protection requires them.
#
# Triggers:
# 1. pull_request_target (opened/synchronize/reopened) — fired by close/reopen
# in release.yml or by direct PR updates
# 2. workflow_run (Release workflow completed) — fallback if close/reopen fails
# to fire pull_request_target events
#
# Security: restricted to release-please branches only. The checkout uses the
# PR's HEAD SHA, which is safe because release-please PRs come from within the
# same repository (not forks) and only modify version/changelog files.
Expand All @@ -16,26 +22,54 @@ on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened]
workflow_run:
workflows: [Release]
types: [completed]

permissions: read-all

jobs:
# Gate: only run for release-please PRs
# Gate: only run for release-please PRs. Finds PR details from either trigger.
should-run:
if: startsWith(github.head_ref, 'release-please--')
if: |
(github.event_name == 'pull_request_target' && startsWith(github.head_ref, 'release-please--')) ||
github.event_name == 'workflow_run'
runs-on: ubuntu-latest
outputs:
head_sha: ${{ steps.find-pr.outputs.head_sha }}
steps:
- run: echo "Running CI for release-please PR"
- name: Find release PR
id: find-pr
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
echo "Triggered by pull_request_target"
echo "head_sha=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
else
# workflow_run trigger — find the open release-please PR
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --json number,headRefName,headRefOid --jq '.[] | select(.headRefName | startswith("release-please--"))' 2>/dev/null || echo "")
if [ -z "$PR" ] || [ "$PR" = "null" ]; then
echo "No open release-please PR found, skipping"
echo "head_sha=" >> "$GITHUB_OUTPUT"
exit 0
fi
HEAD_SHA=$(echo "$PR" | jq -r '.headRefOid')
PR_NUM=$(echo "$PR" | jq -r '.number')
echo "Found release PR #$PR_NUM (sha: $HEAD_SHA)"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
fi

lint-and-check:
needs: [should-run]
if: needs.should-run.outputs.head_sha != ''
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout PR code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ needs.should-run.outputs.head_sha }}

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down Expand Up @@ -64,10 +98,14 @@ jobs:
ci:
runs-on: ubuntu-latest
if: always()
needs: [lint-and-check]
needs: [should-run, lint-and-check]
steps:
- name: Check CI status
run: |
if [[ "${{ needs.should-run.outputs.head_sha }}" == "" ]]; then
echo "No release PR found, skipping"
exit 0
fi
if [[ "${{ needs.lint-and-check.result }}" == "failure" || "${{ needs.lint-and-check.result }}" == "cancelled" ]]; then
echo "CI failed"
exit 1
Expand All @@ -76,6 +114,7 @@ jobs:

codeql:
needs: [should-run]
if: needs.should-run.outputs.head_sha != ''
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
Expand All @@ -84,7 +123,7 @@ jobs:
- name: Checkout PR code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ needs.should-run.outputs.head_sha }}

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
Expand Down
48 changes: 43 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
# When release-please creates or updates a PR via GITHUB_TOKEN, the
# pull_request_target `synchronize` event doesn't always fire. Closing
# and reopening the PR ensures the `reopened` event triggers CI checks.
# We verify both state transitions and retry once on failure.
trigger-pr-checks:
needs: [release-please]
if: needs.release-please.outputs.pr != '' && needs.release-please.outputs.release_created != 'true'
Expand All @@ -45,8 +46,45 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ fromJSON(needs.release-please.outputs.pr).number }}
run: |
echo "Triggering checks on release PR #$PR_NUMBER"
gh pr close "$PR_NUMBER" --repo "$GITHUB_REPOSITORY"
sleep 2
gh pr reopen "$PR_NUMBER" --repo "$GITHUB_REPOSITORY"
echo "Release PR #$PR_NUMBER reopened — checks should now trigger"
trigger_checks() {
echo "Closing release PR #$PR_NUMBER..."
gh pr close "$PR_NUMBER" --repo "$GITHUB_REPOSITORY"

# Wait and verify closed state
for i in 1 2 3; do
sleep 3
STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo "$GITHUB_REPOSITORY")
if [ "$STATE" = "CLOSED" ]; then break; fi
echo " Waiting for close to propagate (attempt $i)..."
done

echo "Reopening release PR #$PR_NUMBER..."
gh pr reopen "$PR_NUMBER" --repo "$GITHUB_REPOSITORY"

# Verify reopened state
sleep 3
STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo "$GITHUB_REPOSITORY")
if [ "$STATE" = "OPEN" ]; then
echo "Release PR #$PR_NUMBER successfully reopened"
return 0
else
echo "WARNING: PR state is $STATE after reopen attempt"
return 1
fi
}

# First attempt
if trigger_checks; then
exit 0
fi

echo "First attempt failed, retrying in 10s..."
sleep 10

# Retry once
if trigger_checks; then
exit 0
fi

echo "ERROR: Failed to reopen PR after 2 attempts"
exit 1
2 changes: 0 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ name: Security
on:
pull_request:
branches: [main]
paths-ignore: ['docs/**', '*.md']
push:
branches: [main]
paths-ignore: ['docs/**', '*.md']
schedule:
- cron: '0 0 * * 1' # Weekly on Monday midnight UTC
workflow_dispatch:
Expand Down
17 changes: 13 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ Response: { "user": { ... }, "group": { ... } }

### GET /api/clips
```
Query params: ?filter=unwatched|watched|favorites&limit=20&offset=0
Query params: ?filter=unwatched|watched|favorites&sort=oldest|round-robin&limit=20&offset=0
Response: { "clips": [...], "hasMore": true }
```
Only returns clips with `status: 'ready'`. Default sort is `oldest` (chronological). `round-robin` interleaves clips across members so no single poster dominates the feed. The `watched` filter sorts by most-recently-watched instead.

Each clip includes: id, originalUrl, title, addedByUsername, addedByAvatar, status, durationSeconds, platform, contentType, createdAt, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers.

### POST /api/clips
Expand Down Expand Up @@ -335,10 +337,10 @@ Response: { "preferences": { ... } }

### POST /api/profile/preferences
```
Request: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false }
Response: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false }
Request: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false, "feedSortOrder": "oldest" }
Response: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false, "feedSortOrder": "oldest" }
```
All fields optional — only provided fields are updated.
All fields optional — only provided fields are updated. `feedSortOrder` accepts `"oldest"` or `"round-robin"`.

### POST /api/profile/avatar
Upload a profile picture as `multipart/form-data`.
Expand Down Expand Up @@ -373,13 +375,20 @@ Response: { "gifs": [{ "id", "title", "url", "stillUrl", "width", "height" }] }
|--------|------|-------------|
| POST | `/api/push/subscribe` | Register a push subscription |
| DELETE | `/api/push/subscribe` | Unregister |
| POST | `/api/push/test` | Send a test notification to current user |

### POST /api/push/subscribe
```
Request: { "endpoint": "...", "keys": { "p256dh": "...", "auth": "..." } }
Response: { "id": "subscription-id" } (201 Created)
```

### POST /api/push/test
Sends a test push notification to the current user after a 10-second delay. Requires at least one active push subscription.
```
Response: { "sent": true, "sentAt": 1740000000000 }
```

## Media

| Method | Path | Description |
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ scrolly/
│ │ │ ├── MemberList.svelte
│ │ │ ├── RetentionPicker.svelte
│ │ │ ├── ClipsManager.svelte
│ │ │ ├── NotificationSettings.svelte # Push toggle + test button
│ │ │ ├── ShortcutManager.svelte # iOS Shortcut config wrapper
│ │ │ ├── ShortcutSheet.svelte # Shortcut setup sheet content
│ │ │ ├── ValidationResults.svelte # Shared validation display
│ │ │ ├── GettingStartedChecklist.svelte
│ │ │ └── SetupDoneState.svelte
│ │ ├── stores/
Expand Down
1 change: 1 addition & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ SQLite database via Drizzle ORM. All IDs are UUIDs stored as text. Timestamps ar
| theme_preference | text | `'system'` / `'light'` / `'dark'`. Default `'system'`. |
| auto_scroll | integer | Boolean (0/1). Default 0. |
| muted_by_default | integer | Boolean (0/1). Default 1. |
| feed_sort_order | text | `'oldest'` / `'round-robin'`. Default `'oldest'`. |
| avatar_path | text | Nullable. Path to uploaded profile picture. |
| removed_at | integer | Nullable. Unix timestamp when removed from group. |
| created_at | integer | Unix timestamp |
Expand Down
1 change: 1 addition & 0 deletions docs/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ VAPID_SUBJECT=mailto:you@example.com
|-------|------|------|
| Server push utility | `src/lib/server/push.ts` | VAPID init, `sendNotification()`, `sendGroupNotification()`, `notifyNewClip()` |
| Subscribe API | `src/routes/api/push/subscribe/+server.ts` | POST/DELETE push subscriptions |
| Test API | `src/routes/api/push/test/+server.ts` | POST test notification to current user |
| Notifications API | `src/routes/api/notifications/+server.ts` | GET notification feed |
| Mark-read API | `src/routes/api/notifications/mark-read/+server.ts` | POST mark as read |
| Unread-count API | `src/routes/api/notifications/unread-count/+server.ts` | GET unread badge count |
Expand Down
Loading