diff --git a/.github/scripts/generate-e2e-matrix.js b/.github/scripts/generate-e2e-matrix.js new file mode 100755 index 00000000..367f9527 --- /dev/null +++ b/.github/scripts/generate-e2e-matrix.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +/** + * Generate E2E test matrix by creating cross-product of locales and shards + * + * Usage: + * node generate-e2e-matrix.js '' + * + * Example: + * node generate-e2e-matrix.js '[{"locale":"zh-hans","secret_project_id":"VERCEL_PROJECT_ID_ZH_HANS"},{"locale":"zh-hant","secret_project_id":"VERCEL_PROJECT_ID_ZH_HANT"}]' 5 + */ + +function generateE2EMatrix(localesJson, shardTotal) { + try { + // Parse the locales input + const locales = JSON.parse(localesJson); + const shards = Array.from({ length: shardTotal }, (_, i) => i + 1); + + // Generate cross-product of locales and shards + const matrix = []; + + for (const locale of locales) { + for (const shard of shards) { + matrix.push({ + ...locale, + shard: shard, + }); + } + } + + const result = { + include: matrix, + }; + + return result; + } catch (error) { + console.error('Error generating matrix:', error.message); + throw error; + } +} + +// Main execution +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.length !== 2) { + console.error( + 'Usage: node generate-e2e-matrix.js ', + ); + console.error( + 'Example: node generate-e2e-matrix.js \'[{"locale":"zh-hans"}]\' 3', + ); + process.exit(1); + } + + const [localesJson, shardTotalStr] = args; + const shardTotal = Number.parseInt(shardTotalStr, 10); + + if (Number.isNaN(shardTotal) || shardTotal < 1) { + console.error('Error: shard-total must be a positive integer'); + process.exit(1); + } + + try { + const matrix = generateE2EMatrix(localesJson, shardTotal); + + // Output results in GitHub Actions format (same as prerelease matrix script) + console.log(`test-matrix=${JSON.stringify(matrix)}`); + } catch (error) { + console.error('Failed to generate matrix:', error.message); + process.exit(1); + } +} + +module.exports = { generateE2EMatrix }; diff --git a/.github/scripts/generate-prerelease-matrix.js b/.github/scripts/generate-prerelease-matrix.js new file mode 100755 index 00000000..a0981917 --- /dev/null +++ b/.github/scripts/generate-prerelease-matrix.js @@ -0,0 +1,219 @@ +#!/usr/bin/env node + +/** + * Script to generate locale deployment matrix for prerelease workflow + * Usage: node generate-prerelease-matrix.js + * + * labels-json: JSON string containing array of PR labels + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +// Default values +const SCRIPT_DIR = __dirname; +const ROOT_DIR = path.resolve(SCRIPT_DIR, '../..'); +const LOCALE_CONFIG_FILE = path.join(ROOT_DIR, '.github/locales-config.json'); + +/** + * Print usage information + */ +function usage() { + console.log(`Usage: ${process.argv[1]} `); + console.log(''); + console.log('Arguments:'); + console.log(' labels-json JSON string containing array of PR labels'); + console.log(''); + console.log('Examples:'); + console.log(` ${process.argv[1]} '["prerelease"]'`); + console.log(` ${process.argv[1]} '["prerelease:en", "prerelease:zh-hans"]'`); + process.exit(1); +} + +/** + * Log messages with timestamp and emoji + */ +function log(message) { + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); + console.error(`🔧 [${timestamp}] ${message}`); +} + +/** + * Check if locale is enabled in config + */ +function isLocaleEnabled(localeConfig, locale) { + return localeConfig[locale]?.enabled === true; +} + +/** + * Get locale configuration value + */ +function getLocaleConfig(localeConfig, locale, field) { + return localeConfig[locale]?.[field] || ''; +} + +/** + * Add locale to matrix + */ +function addLocaleToMatrix(matrix, locale, secretProjectId) { + return [ + ...matrix, + { + locale, + secret_project_id: secretProjectId, + }, + ]; +} + +/** + * Process prerelease labels and generate matrix + */ +function processPrerelease(localeConfig, labels) { + let matrixInclude = []; + + log(`Processing labels: ${JSON.stringify(labels)}`); + + // Check if we should deploy all locales (general prerelease) + const shouldDeployAll = labels.includes('prerelease'); + + if (shouldDeployAll) { + log('📦 General prerelease label found - deploying all enabled locales'); + + // Deploy all enabled locales + for (const locale of Object.keys(localeConfig)) { + if (isLocaleEnabled(localeConfig, locale)) { + const secretProjectId = getLocaleConfig( + localeConfig, + locale, + 'secret_project_id', + ); + + if (secretProjectId) { + matrixInclude = addLocaleToMatrix( + matrixInclude, + locale, + secretProjectId, + ); + log(`✅ Added ${locale} to deployment matrix`); + } else { + log(`⚠️ Skipping ${locale} (missing secret_project_id)`); + } + } else { + log(`⏭️ Skipping ${locale} (not enabled)`); + } + } + } else { + // Check for specific locale labels (prerelease:locale) + const localeLabels = labels.filter((label) => + label.startsWith('prerelease:'), + ); + + if (localeLabels.length > 0) { + log(`🎯 Specific locale labels found: ${localeLabels.join(', ')}`); + + for (const label of localeLabels) { + const locale = label.replace('prerelease:', ''); + + if (isLocaleEnabled(localeConfig, locale)) { + const secretProjectId = getLocaleConfig( + localeConfig, + locale, + 'secret_project_id', + ); + + if (secretProjectId) { + matrixInclude = addLocaleToMatrix( + matrixInclude, + locale, + secretProjectId, + ); + log(`✅ Added ${locale} to deployment matrix`); + } else { + log(`⚠️ Skipping ${locale} (missing secret_project_id)`); + } + } else { + log(`❌ Skipping ${locale} (not enabled or not found)`); + } + } + } else { + log('🚫 No prerelease labels found'); + } + } + + const hasChanges = matrixInclude.length > 0; + + log(`📊 Generated matrix with ${matrixInclude.length} locales`); + + return { + matrixInclude, + hasChanges, + }; +} + +/** + * Main function + */ +function main() { + try { + // Parse command line arguments + const args = process.argv.slice(2); + + if (args.length !== 1) { + console.error('Error: Invalid number of arguments'); + usage(); + } + + const labelsJson = args[0]; + + // Validate and parse labels JSON + let labels; + try { + labels = JSON.parse(labelsJson); + if (!Array.isArray(labels)) { + throw new Error('Labels must be an array'); + } + } catch (error) { + console.error(`Error: Invalid labels JSON: ${error.message}`); + process.exit(1); + } + + // Read locale configuration + let localeConfig; + try { + if (!fs.existsSync(LOCALE_CONFIG_FILE)) { + throw new Error(`Locale config file not found: ${LOCALE_CONFIG_FILE}`); + } + + const configContent = fs.readFileSync(LOCALE_CONFIG_FILE, 'utf8'); + localeConfig = JSON.parse(configContent); + } catch (error) { + console.error(`Error reading locale config: ${error.message}`); + process.exit(1); + } // Process prerelease labels + const result = processPrerelease(localeConfig, labels); + + // Output results in GitHub Actions format + console.log(`matrix={"include":${JSON.stringify(result.matrixInclude)}}`); + + // Validate that we have at least one locale to deploy + if (result.matrixInclude.length === 0) { + log('❌ No enabled locales found to deploy'); + process.exit(1); + } + } catch (error) { + console.error(`💥 Error: ${error.message}`); + process.exit(1); + } +} + +// Run main function if script is executed directly +if (require.main === module) { + main(); +} + +module.exports = { + main, + processPrerelease, + isLocaleEnabled, + getLocaleConfig, +}; diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f1c896e1..6db90c52 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -23,54 +23,27 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + - name: Setup Node + uses: actions/setup-node@v4.4.0 + with: + node-version-file: .nvmrc - name: Generate matrix from locales config id: set-matrix run: | LABELS_JSON='${{ toJson(github.event.pull_request.labels.*.name) }}' echo "LABELS_JSON: $LABELS_JSON" - # Check if we should deploy all locales (general prerelease) - SHOULD_DEPLOY_GENERAL=false - if [[ "${{ github.event_name }}" != "pull_request" ]]; then - SHOULD_DEPLOY_GENERAL=true - elif echo "$LABELS_JSON" | grep -E '"prerelease"' > /dev/null; then - SHOULD_DEPLOY_GENERAL=true - fi - - echo "Should deploy all locales: $SHOULD_DEPLOY_GENERAL" + # Use Node.js script to generate matrix + OUTPUT=$(node .github/scripts/generate-prerelease-matrix.js "$LABELS_JSON") + echo "$OUTPUT" >> $GITHUB_OUTPUT + echo "Generated matrix output: $OUTPUT" - # Read the locales config and generate matrix for enabled locales that should deploy - MATRIX=$(jq -c --argjson should_deploy_general "$SHOULD_DEPLOY_GENERAL" --argjson labels "$LABELS_JSON" ' - to_entries | - map(select(.value.enabled == true)) | - map(. as $item | { - locale: $item.key, - secret_project_id: $item.value.secret_project_id, - should_deploy: ( - $should_deploy_general or - ($labels | any(. == ("prerelease:" + $item.key))) - ) - }) | - map(select(.should_deploy == true)) | - map({locale: .locale, secret_project_id: .secret_project_id}) - ' .github/locales-config.json) - - echo "matrix={\"include\":$MATRIX}" >> $GITHUB_OUTPUT - echo "Generated matrix: {\"include\":$MATRIX}" - - # Check if there are any enabled locales - ENABLED_COUNT=$(echo "$MATRIX" | jq length) - if [ "$ENABLED_COUNT" -eq 0 ]; then - echo "No enabled locales found in locales-config.json" - exit 1 - fi test: needs: generate-matrix - if: needs.generate-matrix.outputs.matrix != '[]' + if: needs.generate-matrix.outputs.matrix != '{"include":[]}' uses: ./.github/workflows/test-e2e.yml with: - matrix-include: ${{ needs.generate-matrix.outputs.matrix }} - shard-total: 5 + matrix-include: ${{ toJson(fromJson(needs.generate-matrix.outputs.matrix).include) }} secrets: inherit deploy: diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 38963112..d7d84971 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -11,28 +11,38 @@ on: description: 'Total number of shards' required: false type: number - default: 3 + default: 1 jobs: - prepare-shards: + prepare-matrix: runs-on: ubuntu-latest outputs: - shard-array: ${{ steps.generate-shards.outputs.shard-array }} + test-matrix: ${{ steps.generate-matrix.outputs.test-matrix }} steps: - - name: Generate shard array - id: generate-shards + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Setup Node + uses: actions/setup-node@v4.4.0 + with: + node-version-file: .nvmrc + + - name: Generate test matrix + id: generate-matrix run: | - shards=$(seq 1 ${{ inputs.shard-total }} | jq -s -c 'map(tonumber)') - echo "shard-array=$shards" >> $GITHUB_OUTPUT + # Use the Node.js script to generate the matrix + output=$(node .github/scripts/generate-e2e-matrix.js '${{ inputs.matrix-include }}' ${{ inputs.shard-total }}) + echo "$output" >> $GITHUB_OUTPUT + echo "Generated matrix: $output" test: - needs: prepare-shards + needs: prepare-matrix runs-on: ubuntu-latest strategy: - matrix: - include: ${{ fromJson(inputs.matrix-include) }} - shard: ${{ fromJson(needs.prepare-shards.outputs.shard-array) }} - name: Test ${{ matrix.locale }} (Shard ${{ matrix.shard }}/${{ inputs.shard-total }}) + matrix: ${{ fromJson(needs.prepare-matrix.outputs.test-matrix) }} + name: Test ${{ matrix.locale }}${{ inputs.shard-total > 1 && format(' (Shard {0}/{1})', matrix.shard, inputs.shard-total) || '' }} steps: - name: Checkout code uses: actions/checkout@v3 @@ -62,6 +72,8 @@ jobs: pnpm --filter @next-i18n/docs playwright:install - name: Run E2E Tests + env: + LOCALE: ${{ matrix.locale }} run: | echo "Running tests for ${{ matrix.locale }} (Shard ${{ matrix.shard }}/${{ inputs.shard-total }})" pnpm --filter @next-i18n/docs test:e2e --shard ${{ matrix.shard }}/${{ inputs.shard-total }}