diff --git a/.github/workflows/chart-ci.yml b/.github/workflows/chart-ci.yml index 4c3c85bbb..9f3b25958 100644 --- a/.github/workflows/chart-ci.yml +++ b/.github/workflows/chart-ci.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Check version alignment shell: bash @@ -64,10 +64,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Helm - uses: azure/setup-helm@v4.3.1 + uses: azure/setup-helm@v5 - name: Add chart dependencies repositories run: | diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml index a3efc238b..e09bd9483 100644 --- a/.github/workflows/chart-release.yml +++ b/.github/workflows/chart-release.yml @@ -18,7 +18,7 @@ jobs: app_version: ${{ steps.tag.outputs.app_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -79,13 +79,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Helm chart artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: helm-chart-package path: ${{ runner.temp }}/helm-artifacts - name: Create chart GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare_release.outputs.tag_name }} name: ${{ needs.prepare_release.outputs.tag_name }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a040c7a31..0859eca33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -25,7 +25,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -41,7 +41,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -57,12 +57,12 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js environment - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: - node-version: "20" + node-version: "24" cache: "npm" - name: Install dependencies @@ -104,7 +104,7 @@ jobs: run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libvips postgresql-client libpq-dev - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -153,7 +153,7 @@ jobs: run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libvips postgresql-client libpq-dev - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -171,7 +171,7 @@ jobs: run: DISABLE_PARALLELIZATION=true bin/rails test:system - name: Keep screenshots from failed system tests - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() with: name: screenshots diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 15bcfbe08..696a8e9f7 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -93,7 +93,7 @@ jobs: fi - name: Upload APK artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app-release-apk path: | @@ -109,7 +109,7 @@ jobs: - name: Upload AAB artifact if: steps.check_secrets.outputs.has_keystore == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app-release-aab path: mobile/build/app/outputs/bundle/release/app-release.aab @@ -122,7 +122,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Flutter uses: subosito/flutter-action@v2 @@ -167,7 +167,7 @@ jobs: echo "For distribution, you need to configure code signing with Apple certificates" >> build/ios-build-info.txt - name: Upload iOS build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ios-build-unsigned path: | diff --git a/.github/workflows/google-play-upload.yml b/.github/workflows/google-play-upload.yml index 14e63eb15..efb762e02 100644 --- a/.github/workflows/google-play-upload.yml +++ b/.github/workflows/google-play-upload.yml @@ -56,7 +56,7 @@ jobs: - name: Download Android AAB artifact if: ${{ steps.check_prereqs.outputs.enabled == 'true' }} - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: app-release-aab path: ${{ runner.temp }}/android-aab diff --git a/.github/workflows/helm-publish.yml b/.github/workflows/helm-publish.yml index f38a786da..9f6c6b3b6 100644 --- a/.github/workflows/helm-publish.yml +++ b/.github/workflows/helm-publish.yml @@ -29,12 +29,12 @@ jobs: app_version: ${{ steps.version.outputs.app_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install Helm - uses: azure/setup-helm@v4.3.1 + uses: azure/setup-helm@v5 - name: Resolve chart and app versions id: version @@ -88,7 +88,7 @@ jobs: helm package charts/sure -d .cr-release-packages - name: Upload packaged chart artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: helm-chart-package path: .cr-release-packages/*.tgz @@ -98,7 +98,7 @@ jobs: - name: Checkout gh-pages if: ${{ inputs.update_gh_pages }} - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: gh-pages path: gh-pages diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index b20f9b6ec..6e058642c 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Check TestFlight credentials id: check_prereqs @@ -293,7 +293,7 @@ jobs: - name: Upload build artifact if: ${{ steps.check_prereqs.outputs.enabled == 'true' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ios-ipa-testflight path: mobile/build/ios/ipa/*.ipa diff --git a/.github/workflows/llm-evals.yml b/.github/workflows/llm-evals.yml index 69c917e1a..13b608336 100644 --- a/.github/workflows/llm-evals.yml +++ b/.github/workflows/llm-evals.yml @@ -101,7 +101,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -204,7 +204,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -320,7 +320,7 @@ jobs: echo "status=$(jq -r '.status' "$JSON_PATH")" >> "$GITHUB_OUTPUT" - name: Upload eval artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: llm-evals-${{ steps.dataset_slug.outputs.slug }}-${{ steps.dataset_slug.outputs.model_slug }} path: | @@ -346,7 +346,7 @@ jobs: steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: path: eval-artifacts pattern: llm-evals-* diff --git a/.github/workflows/mobile-build.yml b/.github/workflows/mobile-build.yml index 38507c273..fe985bb01 100644 --- a/.github/workflows/mobile-build.yml +++ b/.github/workflows/mobile-build.yml @@ -64,21 +64,21 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.ref }} fetch-depth: 0 - name: Download Android APK artifact continue-on-error: true - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: app-release-apk path: ${{ runner.temp }}/mobile-artifacts - name: Download iOS build artifact continue-on-error: true - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: ios-build-unsigned path: ${{ runner.temp }}/ios-build @@ -170,7 +170,7 @@ jobs: ${{ runner.temp }}/release-assets/* - name: Checkout gh-pages branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: gh-pages path: gh-pages diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 3367a457c..0852a7208 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -112,13 +112,13 @@ jobs: echo "Extracted version: $VERSION" - name: Download Android APK artifact - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: app-release-apk path: ${{ runner.temp }}/mobile-artifacts - name: Download iOS build artifact - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: ios-build-unsigned path: ${{ runner.temp }}/ios-build @@ -258,7 +258,7 @@ jobs: done - name: Checkout gh-pages branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: gh-pages path: gh-pages diff --git a/.github/workflows/pipelock.yml b/.github/workflows/pipelock.yml index deef6acd9..7b3b46af9 100644 --- a/.github/workflows/pipelock.yml +++ b/.github/workflows/pipelock.yml @@ -11,7 +11,7 @@ jobs: security-scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: false @@ -29,3 +29,4 @@ jobs: config/locales/views/reports/ docs/hosting/ai.md app/models/provider/binance.rb + workers/preview/package-lock.json diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml new file mode 100644 index 000000000..b36c2cba4 --- /dev/null +++ b/.github/workflows/preview-cleanup.yml @@ -0,0 +1,216 @@ +name: Cleanup PR Previews + +on: + # Run hourly to check for expired previews + schedule: + - cron: '0 * * * *' + + # Immediately cleanup when PR is closed + pull_request: + types: [closed, unlabeled] + + # Allow manual trigger + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to cleanup (optional, cleans all expired if empty)' + required: false + type: string + +permissions: + contents: read + deployments: write + +jobs: + cleanup-on-close: + name: Cleanup closed PR preview + if: github.event_name == 'pull_request' && (github.event.action == 'closed' || (github.event.action == 'unlabeled' && github.event.label.name == 'preview-cf')) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "24" + + - name: Install Wrangler + run: npm install -g wrangler + + - name: Delete preview Worker + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + WORKER_NAME="sure-preview-${{ github.event.pull_request.number }}" + echo "Deleting Worker: $WORKER_NAME" + + # Delete the worker (this also stops any running containers) + wrangler delete --name "$WORKER_NAME" --force || echo "Worker may not exist" + + - name: Delete GitHub Deployment + uses: actions/github-script@v7 + with: + script: | + const environment = `preview-pr-${{ github.event.pull_request.number }}`; + const description = context.payload.action === 'closed' + ? 'PR closed - preview deleted' + : 'preview-cf label removed - preview deleted'; + + try { + // Get deployments for this environment + const { data: deployments } = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + environment: environment + }); + + // Mark all deployments as inactive + for (const deployment of deployments) { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive', + description + }); + } + + console.log(`Marked ${deployments.length} deployments as inactive`); + } catch (error) { + console.log('No deployments to cleanup or error:', error.message); + } + + cleanup-expired: + name: Cleanup expired previews + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "24" + + - name: Install Wrangler + run: npm install -g wrangler + + - name: Cleanup expired previews + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_INPUT: ${{ inputs.pr_number }} + run: | + # If specific PR number provided, only cleanup that one + if [ -n "$PR_INPUT" ]; then + if [[ "$PR_INPUT" =~ ^[1-9][0-9]*$ ]]; then + PR_NUM="$PR_INPUT" + WORKER_NAME="sure-preview-$PR_NUM" + echo "Manually deleting Worker: $WORKER_NAME" + wrangler delete --name "$WORKER_NAME" --force || echo "Worker may not exist" + + # Cleanup GitHub deployment for this PR + echo "Cleaning up GitHub deployment for PR #$PR_NUM" + gh api \ + -X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \ + --jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do + if [ -n "$DEPLOY_ID" ]; then + gh api \ + -X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \ + -f state=inactive \ + -f description="Preview manually deleted" || true + fi + done || echo "No deployments to cleanup or error occurred" + else + echo "Invalid PR number input '$PR_INPUT'; skipping manual cleanup" + fi + + exit 0 + fi + + # Get list of all preview workers + echo "Fetching list of preview workers..." + + # Use Cloudflare API to list workers and read modified_on from the list response. + # The per-script endpoint returns raw script content, not JSON metadata. + WORKERS_RESPONSE=$(curl -fsS -X GET \ + "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts" \ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ + -H "Content-Type: application/json") || { + echo "Failed to fetch preview worker list from Cloudflare" + exit 1 + } + + if ! echo "$WORKERS_RESPONSE" | jq -e '.success == true and (.result | type == "array")' >/dev/null 2>&1; then + echo "Cloudflare API returned an invalid worker list response" + echo "$WORKERS_RESPONSE" | jq -c '.errors // .' + exit 1 + fi + + WORKERS=$(echo "$WORKERS_RESPONSE" | jq -r ' + .result[] + | select(.id | startswith("sure-preview-")) + | [.id, (.modified_on // "")] + | @tsv + ') + + if [ -z "$WORKERS" ]; then + echo "No preview workers found" + exit 0 + fi + + echo "Found preview workers:" + echo "$WORKERS" | cut -f1 + + # Check each worker's deployment time + CUTOFF_TIME=$(date -d '24 hours ago' +%s) + + while IFS=$'\t' read -r WORKER MODIFIED_ON; do + [ -n "$WORKER" ] || continue + echo "Checking $WORKER..." + + if [ -z "$MODIFIED_ON" ]; then + echo "No modified_on timestamp for $WORKER; skipping" + continue + fi + + if ! MODIFIED_TS=$(date -d "$MODIFIED_ON" +%s 2>/dev/null); then + echo "Invalid modified_on timestamp for $WORKER ($MODIFIED_ON); skipping" + continue + fi + + if [ "$MODIFIED_TS" -lt "$CUTOFF_TIME" ]; then + echo "Worker $WORKER is older than 24 hours, deleting..." + if wrangler delete --name "$WORKER" --force; then + # Extract PR number and cleanup GitHub deployment + PR_NUM=$(echo "$WORKER" | sed 's/sure-preview-//') + if [[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]]; then + echo "Cleaning up GitHub deployment for PR #$PR_NUM" + gh api \ + -X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \ + --jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do + gh api \ + -X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \ + -f state=inactive \ + -f description="Preview expired after 24 hours" || true + done || echo "No deployments to cleanup or error occurred" + else + echo "Could not extract a valid PR number from $WORKER; skipping deployment cleanup" + fi + else + echo "Failed to delete $WORKER; skipping deployment status update" + fi + else + echo "Worker $WORKER is still within 24-hour window, keeping..." + fi + done <<< "$WORKERS" + + echo "Cleanup complete" diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 000000000..eff9c847a --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,189 @@ +name: Deploy PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + paths-ignore: + - 'charts/**' + - 'docs/**' + - '*.md' + +jobs: + deploy-preview: + if: contains(github.event.pull_request.labels.*.name, 'preview-cf') + name: Deploy to Cloudflare Containers + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: read + contents: read + pull-requests: write + deployments: write + + steps: + - name: Wait for PR CI to pass + uses: actions/github-script@v7 + with: + script: | + const headSha = context.payload.pull_request.head.sha; + const timeoutMs = 10 * 60 * 1000; + const pollMs = 15 * 1000; + const startedAt = Date.now(); + let lastState = 'not found'; + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + while (Date.now() - startedAt < timeoutMs) { + const { data } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + event: 'pull_request', + head_sha: headSha, + per_page: 20, + }); + + const prRun = data.workflow_runs.find((run) => run.name === 'Pull Request' && run.head_sha === headSha); + + if (prRun) { + lastState = `${prRun.status}/${prRun.conclusion ?? 'pending'}`; + core.info(`Pull Request workflow ${prRun.id}: ${lastState}`); + + if (prRun.status === 'completed') { + if (prRun.conclusion === 'success') { + return; + } + + core.setFailed(`Pull Request workflow concluded with ${prRun.conclusion}`); + return; + } + } + + await sleep(pollMs); + } + + core.setFailed(`Timed out waiting for Pull Request workflow for ${headSha}. Last state: ${lastState}`); + + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Install Wrangler dependencies + working-directory: workers/preview + run: npm install + + - name: Configure preview files for this PR + working-directory: workers/preview + run: | + sed -i "s/\${PR_NUMBER}/${{ github.event.pull_request.number }}/g" wrangler.toml + sed -i "s/\${PR_NUMBER}/${{ github.event.pull_request.number }}/g" src/index.ts + cat wrangler.toml + + - name: Create GitHub Deployment + id: deployment + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + environment: `preview-pr-${{ github.event.pull_request.number }}`, + auto_merge: false, + required_contexts: [], + description: 'PR Preview Deployment' + }); + return deployment.data.id; + result-encoding: string + + - name: Deploy to Cloudflare Containers + id: deploy + working-directory: workers/preview + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + npx wrangler deploy --var "PR_NUMBER:${{ github.event.pull_request.number }}" + + # Get the deployment URL + PREVIEW_URL="https://sure-preview-${{ github.event.pull_request.number }}.${{ secrets.CLOUDFLARE_WORKERS_SUBDOMAIN }}.workers.dev" + echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT" + + - name: Warm preview container + env: + PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} + run: | + echo "Triggering preview wake-up..." + curl -fsS "$PREVIEW_URL/" >/dev/null || true + + - name: Update Deployment Status + if: always() && steps.deployment.outputs.result + uses: actions/github-script@v7 + with: + script: | + const state = '${{ job.status }}' === 'success' ? 'success' : 'failure'; + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.result }}, + state: state, + environment_url: state === 'success' ? '${{ steps.deploy.outputs.preview_url }}' : undefined, + description: state === 'success' ? 'Preview deployed successfully' : 'Preview deployment failed' + }); + + - name: Comment on PR + if: success() + uses: actions/github-script@v7 + with: + script: | + const previewUrl = '${{ steps.deploy.outputs.preview_url }}'; + const commentBody = `## 🚀 Preview Deployment Ready + + Your preview environment has been deployed to Cloudflare Containers with the PR's Docker image. + + **Preview URL:** ${previewUrl} + + > ⏰ This preview is intended to be cleaned up after **24 hours** of the last deployment once the cleanup workflow is live on the default branch. + > 💤 The container will sleep after 30 minutes of inactivity and wake on the next request. + + --- + Deployed from commit ${{ github.event.pull_request.head.sha }}`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.pull_request.number }} + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Preview Deployment Ready') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.pull_request.number }}, + body: commentBody + }); + } + - name: Store cleanup metadata + if: success() + uses: actions/upload-artifact@v6 + with: + name: preview-cleanup-pr-${{ github.event.pull_request.number }} + path: | + workers/preview/wrangler.toml + retention-days: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b7b74e37f..225886dea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -73,15 +73,15 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v5 with: ref: ${{ github.event.inputs.ref || github.ref }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v4 - name: Log in to the container registry - uses: docker/login-action@v3.3.0 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -103,7 +103,6 @@ jobs: BASE_CONFIG+=$'\n'"type=raw,value=latest" else BASE_CONFIG+=$'\n'"type=raw,value=stable" - BASE_CONFIG+=$'\n'"type=raw,value=latest" fi fi fi @@ -119,7 +118,7 @@ jobs: - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@v5.6.0 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: latest=false @@ -133,7 +132,7 @@ jobs: org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app - name: Publish 'linux/${{ matrix.platform }}' image by digest - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v7 id: build with: context: . @@ -159,7 +158,7 @@ jobs: - name: Upload the Docker image digest if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v6 with: name: digest-${{ matrix.platform }} path: ${{ runner.temp }}/digests/* @@ -179,17 +178,17 @@ jobs: steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v4 - name: Download Docker image digests - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: path: ${{ runner.temp }}/digests pattern: digest-* merge-multiple: true - name: Log in to the container registry - uses: docker/login-action@v3.3.0 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -276,19 +275,19 @@ jobs: steps: - name: Download Android APK artifact - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: app-release-apk path: ${{ runner.temp }}/mobile-artifacts - name: Download iOS build artifact - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: ios-build-unsigned path: ${{ runner.temp }}/ios-build - name: Download Helm chart artifact - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v7 with: name: helm-chart-package path: ${{ runner.temp }}/helm-artifacts @@ -339,7 +338,7 @@ jobs: ls -la "${{ runner.temp }}/release-assets/" - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} @@ -426,10 +425,10 @@ jobs: echo "branch=$SOURCE_BRANCH" >> $GITHUB_OUTPUT - name: Check out source branch - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v5 with: ref: ${{ steps.source_branch.outputs.branch }} - token: ${{ secrets.GH_PAT || github.token }} + token: ${{ github.token }} - name: Bump pre-release version run: | diff --git a/.sure-version b/.sure-version index 81fe56438..11a626600 100644 --- a/.sure-version +++ b/.sure-version @@ -1 +1 @@ -0.7.1-alpha.7 +0.7.1-alpha.8 diff --git a/Dockerfile.preview b/Dockerfile.preview new file mode 100644 index 000000000..3b687b964 --- /dev/null +++ b/Dockerfile.preview @@ -0,0 +1,247 @@ +# syntax = docker/dockerfile:1 + +# Preview Dockerfile for Cloudflare Containers +# Includes PostgreSQL and Redis for self-contained development testing + +ARG RUBY_VERSION=3.4.7 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base + +WORKDIR /rails + +# Install base packages including PostgreSQL and Redis servers +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y \ + curl libvips postgresql postgresql-client redis-server libyaml-0-2 procps sudo openssl strace \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set development environment +ARG BUILD_COMMIT_SHA +ENV RAILS_ENV="development" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA} + +# Build stage +FROM base AS build + +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config libyaml-dev \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +COPY .ruby-version Gemfile Gemfile.lock ./ +RUN bundle install \ + && rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git \ + && bundle exec bootsnap precompile --gemfile -j 0 + +COPY . . + +RUN bundle exec bootsnap precompile -j 0 app/ lib/ + +# Precompile assets +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + +# Final stage +FROM base + +# Create rails user and configure PostgreSQL/Redis permissions +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + echo "rails ALL=(ALL) NOPASSWD: /usr/bin/pg_ctlcluster, /usr/bin/redis-server" > /etc/sudoers.d/rails && \ + chmod 0440 /etc/sudoers.d/rails + +# Configure PostgreSQL to allow local connections +RUN PG_HBA=$(find /etc/postgresql -name pg_hba.conf 2>/dev/null | head -1) && \ + if [ -n "$PG_HBA" ]; then \ + echo "local all all trust" > "$PG_HBA" && \ + echo "host all all 127.0.0.1/32 trust" >> "$PG_HBA" && \ + echo "host all all ::1/128 trust" >> "$PG_HBA"; \ + fi + +# Create database directory with correct permissions +RUN mkdir -p /var/run/postgresql && \ + chown -R postgres:postgres /var/run/postgresql && \ + chmod 2775 /var/run/postgresql + +# Copy built artifacts +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# Create preview entrypoint script inline +RUN cat > /rails/bin/preview-entrypoint << 'ENTRYPOINT_EOF' +#!/bin/bash +set -e + +cd /rails + +emit_status() { + if [ -n "$PREVIEW_ORIGIN" ]; then + local stage="$1" + local detail="$2" + local payload + payload=$(STAGE="$stage" DETAIL="$detail" ruby -rjson -e 'print JSON.generate({stage: ENV.fetch("STAGE"), detail: ENV.fetch("DETAIL", "")})' 2>/dev/null) || return 0 + curl -fsS -X POST "$PREVIEW_ORIGIN/_container_event" \ + -H 'content-type: application/json' \ + --data "$payload" >/dev/null || true + fi +} + +trap 'emit_status failed "preview-entrypoint failed on line ${LINENO}"' ERR +emit_status boot "preview-entrypoint started" + +REDIS_READY=0 +POSTGRES_READY=0 + +# Start Redis +echo "Starting Redis..." +emit_status redis-start "starting redis" +sudo redis-server --daemonize yes --bind 127.0.0.1 + +# Wait for Redis to be ready +echo "Waiting for Redis to be ready..." +for i in {1..10}; do + if redis-cli ping > /dev/null 2>&1; then + echo "Redis is ready" + emit_status redis-ready "redis is ready" + REDIS_READY=1 + break + fi + sleep 1 +done + +if [ "$REDIS_READY" -ne 1 ]; then + echo "Redis did not become ready in time" + exit 1 +fi + +# Start PostgreSQL +echo "Starting PostgreSQL..." +emit_status postgres-start "starting postgres" +PG_VERSION=$(ls /etc/postgresql/ | sort -V | tail -1) +if [ -z "$PG_VERSION" ]; then + echo "Could not determine installed PostgreSQL version" + exit 1 +fi +if sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main status > /dev/null 2>&1; then + emit_status postgres-already-running "postgres cluster already running" +else + sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main start +fi + +# Wait for PostgreSQL to be ready +echo "Waiting for PostgreSQL to be ready..." +for i in {1..30}; do + if pg_isready -h localhost -U postgres > /dev/null 2>&1; then + echo "PostgreSQL is ready" + emit_status postgres-ready "postgres is ready" + POSTGRES_READY=1 + break + fi + sleep 1 +done + +if [ "$POSTGRES_READY" -ne 1 ]; then + echo "PostgreSQL did not become ready in time" + exit 1 +fi + +# Create database user and database if they don't exist +echo "Setting up database..." +emit_status db-setup "setting up database" +psql -h localhost -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='rails'" | grep -q 1 || \ + psql -h localhost -U postgres -c "CREATE USER rails WITH SUPERUSER PASSWORD 'rails';" + +psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='sure_development'" | grep -q 1 || \ + psql -h localhost -U postgres -c "CREATE DATABASE sure_development OWNER rails;" + +# Set DATABASE_URL if not already set +export DATABASE_URL="${DATABASE_URL:-postgres://rails:rails@localhost:5432/sure_development}" + +# Set REDIS_URL if not already set +export REDIS_URL="${REDIS_URL:-redis://localhost:6379/0}" + +# Generate SECRET_KEY_BASE if not set +export SECRET_KEY_BASE="${SECRET_KEY_BASE:-$(openssl rand -hex 64)}" + +# Run database migrations +echo "Running database migrations..." +emit_status db-prepare "running rails db:prepare" +/rails/bin/rails db:prepare +emit_status db-prepare-done "rails db:prepare finished" + +# Defer all demo-data creation until after Rails is up so preview can boot first +echo "Checking demo dataset..." +emit_status demo-data-check "checking for default demo user" +DEMO_EMAIL="${DEMO_USER_EMAIL:-user@example.com}" +DEMO_EMAIL_SQL=${DEMO_EMAIL//\'/\'\'} +DEMO_SEED="${DEMO_DATA_SEED:-880}" +DEMO_HAS_USER=0 +DEMO_HAS_DATA=0 + +if psql "$DATABASE_URL" -tAc "SELECT 1 FROM users WHERE email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then + DEMO_HAS_USER=1 + emit_status demo-data-user-present "default demo user already exists" +fi + +if psql "$DATABASE_URL" -tAc "SELECT 1 FROM accounts a JOIN users u ON u.family_id = a.family_id WHERE u.email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then + DEMO_HAS_DATA=1 + emit_status demo-data-skip "demo financial data already exists" +else + emit_status demo-data-deferred "deferring demo data creation until after rails boot" +fi + +# Execute the main command with an internal readiness probe +echo "Starting Rails server..." +emit_status rails-start "starting rails server" +"$@" > /tmp/rails.log 2>&1 & +RAILS_PID=$! + +for i in {1..180}; do + if curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then + emit_status rails-up-ready "rails responded on localhost:3000/up" + + if [ "$DEMO_HAS_USER" -ne 1 ] || [ "$DEMO_HAS_DATA" -ne 1 ]; then + emit_status demo-data-load "creating/backfilling demo dataset in background (seed=${DEMO_SEED})" + ( + ( + DEMO_USER_EMAIL="$DEMO_EMAIL" DEMO_DATA_SEED="$DEMO_SEED" /rails/bin/rails runner ' + email = ENV.fetch("DEMO_USER_EMAIL") + generator = Demo::Generator.new(seed: ENV.fetch("DEMO_DATA_SEED")) + user = User.find_by(email: email) + + unless user + generator.generate_empty_data!(skip_clear: true) + user = User.find_by!(email: email) + end + + has_accounts = user.family.accounts.exists? + generator.generate_new_user_data_for!(user.family, email: user.email) unless has_accounts + ' + ) > /tmp/demo-data.log 2>&1 && \ + emit_status demo-data-ready "default demo dataset loaded in background" || \ + emit_status demo-data-failed "background demo dataset load failed" + ) & + fi + + break + fi + sleep 1 +done + +if ! curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then + emit_status rails-up-timeout "rails did not answer localhost:3000/up in time" + emit_status rails-process-status "$(ps -o pid=,ppid=,stat=,comm=,args= -p "$RAILS_PID" 2>/dev/null | tr -s ' ' | sed 's/^ //')" + emit_status rails-process-wchan "$(cat /proc/$RAILS_PID/wchan 2>/dev/null | tr '\n' ' ' | cut -c 1-200)" + emit_status rails-process-children "$(ps -o pid=,ppid=,stat=,comm=,args= --ppid "$RAILS_PID" 2>/dev/null | tail -n +2 | tr '\n' '|' | cut -c 1-600)" + emit_status rails-socket-state "$(ruby -e 'hex="0BB8"; rows=File.readlines("/proc/net/tcp")+File.readlines("/proc/net/tcp6"); hits=rows.select{|l| l.include?(":#{hex} ")}.map{|l| l.strip.split[3] rescue nil}.compact; puts(hits.empty? ? "no-listener" : hits.join(","))' 2>&1 | tr '\n' ' ' | cut -c 1-400)" + emit_status rails-log-tail "$(tail -n 40 /tmp/rails.log 2>&1 | sed 's/"/'"'"'/g' | tr '\n' ' ' | cut -c 1-1200)" +fi + +wait "$RAILS_PID" +ENTRYPOINT_EOF +RUN chmod 755 /rails/bin/preview-entrypoint && chown rails:rails /rails/bin/preview-entrypoint + +USER 1000:1000 + +ENTRYPOINT ["/rails/bin/preview-entrypoint"] + +EXPOSE 3000 +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] diff --git a/app/components/UI/account/activity_date.html.erb b/app/components/UI/account/activity_date.html.erb index f563801ab..6fd158d53 100644 --- a/app/components/UI/account/activity_date.html.erb +++ b/app/components/UI/account/activity_date.html.erb @@ -21,7 +21,7 @@
No balance data available for this date
+<%= t(".no_balance_data") %>
<% end %> diff --git a/app/components/UI/account/chart.html.erb b/app/components/UI/account/chart.html.erb index efcdca7d6..54933dcfe 100644 --- a/app/components/UI/account/chart.html.erb +++ b/app/components/UI/account/chart.html.erb @@ -50,7 +50,7 @@ data-time-series-chart-data-value="<%= series.to_json %>"> <% else %>No data available
+<%= t(".no_data_available") %>
- <%= pluralize(sso_provider.errors.count, "error") %> prohibited this provider from being saved: + <%= t("admin.sso_providers.form.errors_title", count: sso_provider.errors.count) %>
Unique identifier (lowercase, numbers, underscores only)
+<%= t("admin.sso_providers.form.name_help") %>
Lucide icon name for the login button
+ label: t("admin.sso_providers.form.icon_label"), + placeholder: t("admin.sso_providers.form.icon_placeholder") %> +<%= t("admin.sso_providers.form.icon_help") %>
OIDC issuer URL (validates .well-known/openid-configuration)
+<%= t("admin.sso_providers.form.issuer_help") %>
Leave blank to keep existing secret
+<%= t("admin.sso_providers.form.client_secret_help_existing") %>
<% end %><%= "#{request.base_url}/auth/#{sso_provider.name.presence || 'PROVIDER_NAME'}/callback" %>
Configure this URL in your identity provider
+<%= t("admin.sso_providers.form.redirect_uri_help") %>
<%= "#{request.base_url}/auth/#{sso_provider.name.presence || 'PROVIDER_NAME'}/callback" %>
Configure this URL as the Assertion Consumer Service URL in your IdP
+<%= t("admin.sso_providers.form.saml_sp_callback_url_help") %>
- Manage single sign-on authentication providers for your instance. + <%= t(".description") %> <% unless FeatureFlags.db_sso_providers? %> - Changes require a server restart to take effect. + <%= t(".restart_required") %> <% end %>
- <%= settings_section title: "Configured Providers" do %> + <%= settings_section title: t(".configured_providers") do %> <% if @sso_providers.any? %>No SSO providers configured yet.
+<%= t(".no_providers_message") %>
Database-backed providers
-Load providers from database instead of YAML config
+<%= t(".db_backed_providers") %>
+<%= t(".db_backed_providers_description") %>
- Set AUTH_PROVIDERS_SOURCE=db to enable database-backed providers.
- This allows changes without server restarts.
+ <%= t(".db_backed_providers_help_html") %>
Assistant reasoning
+<%= t(".assistant_reasoning") %>
<%= icon("chevron-down", class: "group-open:transform group-open:rotate-180") %>Tool Calls
+<%= t(".tool_calls") %>
Function:
+<%= t(".function") %>
<%= tool_call.function_name %>
-Arguments:
+<%= t(".arguments") %>
<%= tool_call.function_arguments %>