Skip to content
Closed
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
75 changes: 75 additions & 0 deletions .github/scripts/generate-e2e-matrix.js
Original file line number Diff line number Diff line change
@@ -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 '<locale-matrix-json>' <shard-total>
*
* 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 <locale-matrix-json> <shard-total>',
);
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 };
219 changes: 219 additions & 0 deletions .github/scripts/generate-prerelease-matrix.js
Original file line number Diff line number Diff line change
@@ -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>
*
* 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]} <labels-json>`);
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,
};
47 changes: 10 additions & 37 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading