diff --git a/.github/workflows/block-merge.yml b/.github/workflows/block-merge.yml new file mode 100644 index 000000000..00f2c4d57 --- /dev/null +++ b/.github/workflows/block-merge.yml @@ -0,0 +1,34 @@ +name: Block WIP/Draft Merges + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review] + +permissions: + pull-requests: write + checks: write + +jobs: + block-merge: + runs-on: ubuntu-latest + steps: + - name: Check if PR should be blocked + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const title = pr.title || ''; + const isDraft = pr.draft; + const labels = pr.labels.map(l => l.name); + + const hasDoNotMerge = labels.includes('do-not-merge'); + const hasWIPInTitle = /\b(wip|do not merge)\b/i.test(title); + + const shouldBlock = isDraft || hasDoNotMerge || hasWIPInTitle; + + if (shouldBlock) { + core.setFailed('This PR is blocked from merging: ' + + (isDraft ? 'Draft PR' : '') + + (hasDoNotMerge ? ' do-not-merge label' : '') + + (hasWIPInTitle ? ' WIP in title' : '')); + } diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml index e1f082fa1..e4bcac237 100644 --- a/.github/workflows/label-issues.yml +++ b/.github/workflows/label-issues.yml @@ -1,13 +1,17 @@ -name: Label Issues +name: Label Issues and PRs on: issues: types: - opened - transferred + pull_request: + types: + - opened permissions: issues: write + pull-requests: write jobs: label-issue: @@ -17,58 +21,75 @@ jobs: uses: actions/github-script@v7 with: script: | - // Define the mapping from repo/template value to label const labelMap = { 'supabase/auth-js': 'auth-js', 'supabase/functions-js': 'functions-js', 'supabase/postgrest-js': 'postgrest-js', 'supabase/storage-js': 'storage-js', 'supabase/realtime-js': 'realtime-js', + 'supabase/supabase-js': 'supabase-js', 'auth-js': 'auth-js', 'functions-js': 'functions-js', 'postgrest-js': 'postgrest-js', 'storage-js': 'storage-js', 'realtime-js': 'realtime-js', + 'supabase-js': 'supabase-js', + }; + + const scopeToLabel = { + 'auth': 'auth-js', + 'functions': 'functions-js', + 'postgrest': 'postgrest-js', + 'storage': 'storage-js', + 'realtime': 'realtime-js', + 'supabase': 'supabase-js', }; let labels = []; + const isPR = !!context.payload.pull_request; + const item = context.payload.pull_request || context.payload.issue; + const itemNumber = item.number; - const oldRepoFullName = context.payload.changes?.old_repository?.full_name; - if (oldRepoFullName) { - const fullNameLower = (oldRepoFullName || '').toLowerCase(); - const shortName = fullNameLower.split('/')?.[1]; - console.log('old_repository', fullNameLower, shortName); - const transferLabel = labelMap[fullNameLower] || labelMap[shortName]; - if (transferLabel) labels.push(transferLabel); - } else { - // Label based on "Library affected" field in the issue body - const body = context.payload.issue.body || ''; - const match = body.match(/### Library affected\s*\n+([\s\S]*?)(\n###|\n$)/i); - if (match) { - const libsRaw = match[1]; - // Split by comma, semicolon, or newlines - const libs = libsRaw.split(/,|;|\n/).map(s => s.trim().toLowerCase()).filter(Boolean); - for (const lib of libs) { - if (labelMap[lib]) labels.push(labelMap[lib]); + if (isPR) { + const title = item.title || ''; + const scopeMatch = title.match(/^[a-z]+\(([a-z]+)\):/); + if (scopeMatch) { + const scope = scopeMatch[1]; + if (scopeToLabel[scope]) { + labels.push(scopeToLabel[scope]); } } - // Check the title for "[migration]" - const title = context.payload.issue.title || ''; - if (title.toLowerCase().includes('[migration]')) { - labels.push('migration'); + } else { + const oldRepoFullName = context.payload.changes?.old_repository?.full_name; + if (oldRepoFullName) { + const fullNameLower = (oldRepoFullName || '').toLowerCase(); + const shortName = fullNameLower.split('/')?.[1]; + const transferLabel = labelMap[fullNameLower] || labelMap[shortName]; + if (transferLabel) labels.push(transferLabel); + } else { + const body = item.body || ''; + const match = body.match(/### Library affected\s*\n+([\s\S]*?)(\n###|\n$)/i); + if (match) { + const libsRaw = match[1]; + const libs = libsRaw.split(/,|;|\n/).map(s => s.trim().toLowerCase()).filter(Boolean); + for (const lib of libs) { + if (labelMap[lib]) labels.push(labelMap[lib]); + } + } + const title = item.title || ''; + if (title.toLowerCase().includes('[migration]')) { + labels.push('migration'); + } } } - // Remove duplicates labels = [...new Set(labels)]; if (labels.length > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.payload.issue.number, + issue_number: itemNumber, labels, }); - } else { - console.log('No matching label found; no label added.'); } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bab02ffb7..f96b8d2c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,6 +19,8 @@ jobs: release-stable: # stable releases can only be manually triggered if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest + outputs: + released_version: ${{ steps.extract-version.outputs.version }} permissions: contents: read id-token: write @@ -99,14 +101,25 @@ jobs: exit 1 fi - - name: Release & create PR + - name: Release stable version env: NPM_CONFIG_PROVENANCE: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }} + shell: bash + run: npm run release-stable -- --versionSpecifier "${{ github.event.inputs.version_specifier }}" + + - name: Extract released version + id: extract-version + shell: bash run: | - npm run release-stable -- --versionSpecifier "${{ github.event.inputs.version_specifier }}" + set -euo pipefail + VERSION=$(cat .release-version) + if [[ -z "$VERSION" ]]; then + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Summary if: ${{ success() }} @@ -150,7 +163,7 @@ jobs: workflow_id: 'update-js-libs.yml', ref: 'master', inputs: { - version: '${{ github.event.inputs.version_specifier }}', + version: '${{ needs.release-stable.outputs.released_version }}', source: 'supabase-js-stable-release' } }); @@ -181,7 +194,7 @@ jobs: workflow_id: 'docs-js-libs-update.yml', ref: 'master', inputs: { - version: '${{ github.event.inputs.version_specifier }}', + version: '${{ needs.release-stable.outputs.released_version }}', source: 'supabase-js-stable-release' } }); @@ -276,7 +289,19 @@ jobs: uses: ./.github/workflows/slack-notify.yml secrets: inherit with: - subject: 'Stable Release' + title: 'Stable Release' + status: 'failure' + + notify-stable-success: + name: Notify Slack for Stable success + needs: release-stable + if: ${{ github.event_name == 'workflow_dispatch' && needs.release-stable.result == 'success' }} + uses: ./.github/workflows/slack-notify.yml + secrets: inherit + with: + title: 'Stable Release' + status: 'success' + version: ${{ needs.release-stable.outputs.released_version }} notify-canary-failure: name: Notify Slack for Canary failure @@ -285,4 +310,5 @@ jobs: uses: ./.github/workflows/slack-notify.yml secrets: inherit with: - subject: 'Canary Release' + title: 'Canary Release' + status: 'failure' diff --git a/.github/workflows/slack-notify.yml b/.github/workflows/slack-notify.yml index d228ace82..8c7419c38 100644 --- a/.github/workflows/slack-notify.yml +++ b/.github/workflows/slack-notify.yml @@ -3,10 +3,19 @@ name: Reusable Slack Notification on: workflow_call: inputs: - subject: - description: 'Short subject describing what failed (e.g., Canary Release)' + title: + description: 'Short title (e.g., Stable Release, Canary Release)' required: true type: string + status: + description: 'Status for the notification (success|failure|info)' + required: false + default: 'info' + type: string + version: + description: 'Version string to display (e.g., v1.2.3 or 1.2.3-canary.1)' + required: false + type: string jobs: notify: @@ -14,17 +23,40 @@ jobs: steps: - name: Send Slack notification run: | + TITLE="${{ inputs.title }}" + STATUS="${{ inputs.status }}" + VERSION_INPUT="${{ inputs.version }}" + + STATUS_ICON="" + STATUS_PREFIX="" + case "$STATUS" in + success) + STATUS_ICON="✅"; + STATUS_PREFIX="succeeded"; + ;; + failure) + STATUS_ICON="❌"; + STATUS_PREFIX="failed"; + ;; + *) + STATUS_ICON="ℹ️"; + STATUS_PREFIX="update"; + ;; + esac + VERSION_VALUE=${VERSION_INPUT:-n/a} + VERSION_LINE=", version ${VERSION_VALUE}" + payload=$(cat <