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
34 changes: 34 additions & 0 deletions .github/workflows/block-merge.yml
Original file line number Diff line number Diff line change
@@ -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' : ''));
}
75 changes: 48 additions & 27 deletions .github/workflows/label-issues.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.');
}
38 changes: 32 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }}
Expand Down Expand Up @@ -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'
}
});
Expand Down Expand Up @@ -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'
}
});
Expand Down Expand Up @@ -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
Expand All @@ -285,4 +310,5 @@ jobs:
uses: ./.github/workflows/slack-notify.yml
secrets: inherit
with:
subject: 'Canary Release'
title: 'Canary Release'
status: 'failure'
45 changes: 39 additions & 6 deletions .github/workflows/slack-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,69 @@ 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:
runs-on: ubuntu-latest
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 <<EOF
{
"text": " ${{ inputs.subject }} failed in ${{ github.repository }}",
"text": "${STATUS_ICON} ${TITLE} ${STATUS_PREFIX}${VERSION_LINE} in ${{ github.repository }}",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "❌ ${{ inputs.subject }} failed", "emoji": true }
"text": { "type": "plain_text", "text": "${STATUS_ICON} ${TITLE} ${STATUS_PREFIX}", "emoji": true }
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": "The workflow run failed. See details below. @group-client-libs" }
"text": { "type": "mrkdwn", "text": "See workflow details below. @group-client-libs" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Repository*\n${{ github.repository }}" },
{ "type": "mrkdwn", "text": "*Workflow*\n${{ github.workflow }}" },
{ "type": "mrkdwn", "text": "*Ref*\n${{ github.ref_name }}" },
{ "type": "mrkdwn", "text": "*Actor*\n${{ github.actor }}" }
{ "type": "mrkdwn", "text": "*Actor*\n${{ github.actor }}" },
{ "type": "mrkdwn", "text": "*Version*\n${VERSION_VALUE}" }
]
},
{
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/stale.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Mark as Stale and Close Stale Issues

on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:

permissions:
issues: write
pull-requests: write

jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 365
days-before-close: 180
stale-issue-message: 'This issue has been automatically marked as stale because it has not had any activity for 1 year. It will be closed in 6 months if no further activity occurs. If this issue is still relevant, please comment to keep it open.'
close-issue-message: 'This issue has been automatically closed due to inactivity. If you believe this is still relevant, please reopen or create a new issue.'
stale-issue-label: 'stale'
exempt-issue-labels: 'security,needs-discussion,pinned'
exempt-all-assignees: true
remove-stale-when-updated: true
operations-per-run: 100
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ testem.log
/typings
.env

# release artifacts
.release-version

# System Files
.DS_Store
Thumbs.db
Expand Down
8 changes: 8 additions & 0 deletions scripts/release-stable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { releaseVersion, releaseChangelog, releasePublish } from 'nx/release'
import { execSync } from 'child_process'
import { writeFile } from 'fs/promises'

function getLastStableTag(): string {
try {
Expand Down Expand Up @@ -163,6 +164,13 @@ function safeExec(cmd: string, opts = {}) {

const version = result.workspaceChangelog?.releaseVersion.rawVersion || workspaceVersion

// Write version to file for CI to read
try {
await writeFile('.release-version', version ?? '', 'utf-8')
} catch (error) {
console.error('❌ Failed to write release version to file', error)
}

// Validate version to prevent command injection
if (
!version ||
Expand Down