diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 98469cd689c..6980f96603f 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -9,11 +9,22 @@ concurrency: group: ci-release-${{ github.ref_name }} cancel-in-progress: false +# Workflow-level permissions set the ceiling for the reusable ci.yml. +# id-token is never in the default token, so it must be granted explicitly +# here — otherwise the ci: job's `permissions:` block exceeds the caller +# workflow's permissions and GitHub rejects the run with startup_failure. +permissions: + actions: read + contents: write + packages: write + id-token: write + jobs: ci: uses: ./.github/workflows/ci.yml secrets: inherit permissions: + actions: read contents: write packages: write id-token: write diff --git a/.github/workflows/cleanup-stripe-test-accounts.yml b/.github/workflows/cleanup-stripe-test-accounts.yml deleted file mode 100644 index 2f50a286125..00000000000 --- a/.github/workflows/cleanup-stripe-test-accounts.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Cleanup Stripe Test Accounts - -on: - schedule: - # Run twice daily at 3 AM and 3 PM UTC - - cron: '0 3,15 * * *' - workflow_dispatch: # Allow manual trigger - inputs: - dry_run: - description: "Preview deletions without deleting accounts" - required: false - default: "true" - type: choice - options: - - "true" - - "false" - -jobs: - cleanup: - name: Delete old test accounts - runs-on: ubuntu-latest - if: github.repository == 'TryGhost/Ghost' - steps: - - name: Cleanup old Stripe Connect test accounts - env: - STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} - DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} - run: | - set -euo pipefail - if [ -z "${STRIPE_SECRET_KEY:-}" ]; then - echo "STRIPE_SECRET_KEY is not set" - exit 1 - fi - - DRY_RUN_NORMALIZED=$(echo "${DRY_RUN:-false}" | tr '[:upper:]' '[:lower:]') - if [ "$DRY_RUN_NORMALIZED" = "true" ]; then - echo "Running in dry-run mode (no accounts will be deleted)" - fi - - # Delete test accounts older than 24 hours - # Accounts are named like: test-{runId}-{parallelIndex}@example.com - - MAX_AGE_HOURS=24 - MAX_AGE_SECONDS=$((MAX_AGE_HOURS * 60 * 60)) - NOW=$(date +%s) - DELETED=0 - WOULD_DELETE=0 - FAILED_DELETES=0 - DELETE_QUEUE="" - - echo "Fetching Stripe Connect test accounts..." - - # Paginate through all accounts - HAS_MORE=true - STARTING_AFTER="" - - while [ "$HAS_MORE" = "true" ]; do - if [ -z "$STARTING_AFTER" ]; then - if ! RESPONSE=$(curl -sS -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/accounts?limit=100"); then - echo "Failed to fetch Stripe accounts" - exit 1 - fi - else - if ! RESPONSE=$(curl -sS -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/accounts?limit=100&starting_after=$STARTING_AFTER"); then - echo "Failed to fetch Stripe accounts (starting_after=$STARTING_AFTER)" - exit 1 - fi - fi - - # Check for API errors - ERROR=$(echo "$RESPONSE" | jq -r '.error.message // empty') - if [ -n "$ERROR" ]; then - echo "Stripe API error: $ERROR" - exit 1 - fi - - # Extract account data - handle null/missing data array gracefully - ACCOUNTS=$(echo "$RESPONSE" | jq -c '.data // [] | .[]') - HAS_MORE=$(echo "$RESPONSE" | jq -r '.has_more // false') - PAGE_LAST_ID=$(echo "$RESPONSE" | jq -r '.data[-1].id // empty') - - while IFS= read -r account; do - [ -z "$account" ] && continue - - ID=$(echo "$account" | jq -r '.id') - EMAIL=$(echo "$account" | jq -r '.email // ""') - CREATED=$(echo "$account" | jq -r '.created') - - # Check if this is a test account (matches our naming pattern) - if [[ "$EMAIL" =~ ^test-.*@example\.com$ ]]; then - if ! [[ "$CREATED" =~ ^[0-9]+$ ]]; then - echo "Skipping $ID ($EMAIL): invalid created timestamp '$CREATED'" - continue - fi - - AGE=$((NOW - CREATED)) - if [ "$AGE" -gt "$MAX_AGE_SECONDS" ]; then - DELETE_QUEUE="${DELETE_QUEUE}${ID}|${EMAIL}|$((AGE / 3600))"$'\n' - fi - fi - done <<< "$ACCOUNTS" - - if [ "$HAS_MORE" = "true" ]; then - if [ -z "$PAGE_LAST_ID" ]; then - echo "Pagination indicated more results, but no last account id was returned" - exit 1 - fi - STARTING_AFTER="$PAGE_LAST_ID" - fi - done - - while IFS='|' read -r ID EMAIL AGE_HOURS; do - [ -z "${ID:-}" ] && continue - - if [ "$DRY_RUN_NORMALIZED" = "true" ]; then - echo "Would delete $ID ($EMAIL) - age: ${AGE_HOURS} hours" - WOULD_DELETE=$((WOULD_DELETE + 1)) - continue - fi - - echo "Deleting $ID ($EMAIL) - age: ${AGE_HOURS} hours" - if ! DELETE_RESPONSE=$(curl -sS -X DELETE -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/accounts/$ID"); then - echo "Failed to delete $ID ($EMAIL): request failed" - FAILED_DELETES=$((FAILED_DELETES + 1)) - continue - fi - - DELETE_ERROR=$(echo "$DELETE_RESPONSE" | jq -r '.error.message // empty') - DELETED_FLAG=$(echo "$DELETE_RESPONSE" | jq -r '.deleted // false') - if [ -n "$DELETE_ERROR" ] || [ "$DELETED_FLAG" != "true" ]; then - echo "Failed to delete $ID ($EMAIL): ${DELETE_ERROR:-unexpected response}" - FAILED_DELETES=$((FAILED_DELETES + 1)) - continue - fi - - DELETED=$((DELETED + 1)) - done <<< "$DELETE_QUEUE" - - if [ "$DRY_RUN_NORMALIZED" = "true" ]; then - echo "Dry run complete. Would delete $WOULD_DELETE old test accounts" - exit 0 - fi - - echo "Deleted $DELETED old test accounts" - if [ "$FAILED_DELETES" -gt 0 ]; then - echo "Failed to delete $FAILED_DELETES accounts" - exit 1 - fi diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx index 886823be8e1..1ba44e9bef6 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx @@ -55,10 +55,6 @@ const features: Feature[] = [{ title: 'Drip Sequences', description: 'Enable welcome email drip sequences', flag: 'dripSequences' -}, { - title: 'Welcome Emails Design Customization', - description: 'Enable design customization options for welcome emails', - flag: 'welcomeEmailsDesignCustomization' }, { title: 'Picture Element', description: 'Use the HTML picture element to serve modern image formats (AVIF, WebP) with automatic fallbacks', diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx index 0288cfce9b8..47be9b31311 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx @@ -3,7 +3,6 @@ import React, {useEffect, useRef} from 'react'; import TopLevelGroup from '../../top-level-group'; import WelcomeEmailCustomizeModal from './member-emails/welcome-email-customize-modal'; import WelcomeEmailModal from './member-emails/welcome-email-modal'; -import useFeatureFlag from '../../../hooks/use-feature-flag'; import useQueryParams from '../../../hooks/use-query-params'; import {APIError} from '@tryghost/admin-x-framework/errors'; import {Button, ConfirmationModal, Icon, Table, TableRow, Toggle, showToast, withErrorBoundary} from '@tryghost/admin-x-design-system'; @@ -133,7 +132,6 @@ const MemberEmailsTable: React.FC<{ }; const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => { - const hasDesignCustomization = useFeatureFlag('welcomeEmailsDesignCustomization'); const {settings, config} = useGlobalData(); const [siteTitle] = getSettingValues(settings, ['title']); const verifyEmailToken = useQueryParams().getParam('verifyEmail'); @@ -274,19 +272,18 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => { // Get email to display (existing or default for preview) const freeEmailForDisplay = freeWelcomeEmail || getDefaultWelcomeEmailRecord('free', siteTitle); const paidEmailForDisplay = paidWelcomeEmail || getDefaultWelcomeEmailRecord('paid', siteTitle); - const customizeButton = hasDesignCustomization ? ( -