diff --git a/.github/workflows/aws-cdk-deploy.yml b/.github/workflows/aws-cdk-deploy.yml new file mode 100644 index 0000000..d425f10 --- /dev/null +++ b/.github/workflows/aws-cdk-deploy.yml @@ -0,0 +1,640 @@ +name: โ˜๏ธ AWS CDK Deployment + +on: + workflow_call: + inputs: + # Core Configuration + aws-region: + description: "AWS region for deployment" + type: string + required: false + default: "ap-southeast-2" + cdk-stack-name: + description: "CDK stack name to deploy (required)" + type: string + required: true + environment-target: + description: "Target environment (staging/production/development)" + type: string + required: false + default: "development" + + # Deployment Control + approval-required: + description: "Require manual approval before deployment" + type: boolean + required: false + default: true + destroy-mode: + description: "Destroy stack instead of deploying" + type: boolean + required: false + default: false + bootstrap-stack: + description: "Bootstrap CDK environment before deployment" + type: boolean + required: false + default: false + + # Node.js and CDK Configuration + node-version: + description: "Node.js version to use" + type: string + required: false + default: "18" + cdk-version: + description: "Pin specific CDK version (optional)" + type: string + required: false + default: "" + + # Advanced Configuration + context-values: + description: "CDK context values as JSON object" + type: string + required: false + default: "{}" + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + + secrets: + aws-access-key-id: + description: "AWS access key ID" + required: true + aws-secret-access-key: + description: "AWS secret access key" + required: true + cfn-execution-role: + description: "CloudFormation execution role ARN (optional, for cross-account deployments)" + required: false + + outputs: + stack-outputs: + description: "CloudFormation stack outputs as JSON" + value: ${{ jobs.deploy.outputs.stack-outputs }} + deployment-status: + description: "Deployment status (success/failed/destroyed)" + value: ${{ jobs.deploy.outputs.deployment-status }} + +jobs: + # Validate inputs and prepare deployment configuration + prepare: + name: ๐Ÿ” Prepare CDK Deployment + runs-on: ubuntu-latest + outputs: + node-setup: ${{ steps.node-config.outputs.setup }} + deployment-strategy: ${{ steps.deployment-config.outputs.strategy }} + requires-approval: ${{ steps.deployment-config.outputs.requires-approval }} + cdk-command: ${{ steps.deployment-config.outputs.cdk-command }} + context-args: ${{ steps.context-config.outputs.args }} + steps: + - name: Validate required inputs + run: | + echo "๐Ÿ” Validating deployment configuration..." + + if [ -z "${{ inputs.cdk-stack-name }}" ]; then + echo "โŒ Error: cdk-stack-name is required" + exit 1 + fi + + # Validate environment target + case "${{ inputs.environment-target }}" in + staging|production|development) + echo "โœ… Environment target: ${{ inputs.environment-target }}" + ;; + *) + echo "โŒ Error: environment-target must be one of: staging, production, development" + exit 1 + ;; + esac + + # Validate context JSON if provided + if [ "${{ inputs.context-values }}" != "{}" ]; then + echo '${{ inputs.context-values }}' | jq . > /dev/null + if [ $? -ne 0 ]; then + echo "โŒ Error: context-values must be valid JSON" + exit 1 + fi + fi + + echo "โœ… All inputs validated successfully" + + - name: Configure Node.js setup + id: node-config + run: | + echo "๐Ÿ”ง Configuring Node.js environment..." + + # Validate Node.js version + case "${{ inputs.node-version }}" in + 16|18|20) + echo "setup=standard" >> $GITHUB_OUTPUT + echo "โœ… Using Node.js ${{ inputs.node-version }}" + ;; + *) + echo "โš ๏ธ Warning: Unusual Node.js version ${{ inputs.node-version }}, proceeding anyway" + echo "setup=custom" >> $GITHUB_OUTPUT + ;; + esac + + - name: Configure deployment strategy + id: deployment-config + run: | + echo "๐Ÿ“‹ Configuring deployment strategy..." + + # Determine approval requirement + if [ "${{ inputs.environment-target }}" = "production" ] && [ "${{ inputs.approval-required }}" = "true" ]; then + echo "requires-approval=true" >> $GITHUB_OUTPUT + echo "๐Ÿ”’ Production deployment requires manual approval" + else + echo "requires-approval=false" >> $GITHUB_OUTPUT + echo "๐Ÿš€ Deployment will proceed automatically" + fi + + # Determine deployment strategy + if [ "${{ inputs.destroy-mode }}" = "true" ]; then + echo "strategy=destroy" >> $GITHUB_OUTPUT + echo "cdk-command=destroy" >> $GITHUB_OUTPUT + echo "๐Ÿ’ฅ Strategy: Destroy stack" + else + echo "strategy=deploy" >> $GITHUB_OUTPUT + echo "cdk-command=deploy" >> $GITHUB_OUTPUT + echo "๐Ÿ—๏ธ Strategy: Deploy stack" + fi + + - name: Configure CDK context + id: context-config + run: | + echo "โš™๏ธ Configuring CDK context..." + + context_args="" + + # Add environment-specific context + context_args="$context_args --context environment=${{ inputs.environment-target }}" + + # Add custom context values + if [ "${{ inputs.context-values }}" != "{}" ]; then + echo '${{ inputs.context-values }}' | jq -r 'to_entries[] | "--context \(.key)=\(.value)"' | while read -r ctx; do + context_args="$context_args $ctx" + done + fi + + echo "args=$context_args" >> $GITHUB_OUTPUT + echo "โœ… Context arguments configured" + + # Bootstrap CDK environment if required + bootstrap: + name: ๐Ÿฅพ CDK Bootstrap + runs-on: ubuntu-latest + needs: prepare + if: inputs.bootstrap-stack == true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ secrets.cfn-execution-role }} + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Node.js dependencies..." + + verbose="" + if [ "${{ inputs.debug }}" = "true" ]; then + verbose="--verbose" + fi + + npm ci $verbose + + - name: Install or update CDK CLI + run: | + echo "๐Ÿ”ง Setting up AWS CDK CLI..." + + if [ -n "${{ inputs.cdk-version }}" ]; then + echo "๐Ÿ“Œ Installing CDK version: ${{ inputs.cdk-version }}" + npm install -g aws-cdk@${{ inputs.cdk-version }} + else + echo "๐Ÿ“ฆ Installing latest CDK version" + npm install -g aws-cdk + fi + + cdk --version + + - name: Bootstrap CDK environment + run: | + echo "๐Ÿฅพ Bootstrapping CDK environment..." + + verbose="" + if [ "${{ inputs.debug }}" = "true" ]; then + verbose="--verbose" + fi + + role_args="" + if [ -n "${{ secrets.cfn-execution-role }}" ]; then + role_args="--cloudformation-execution-policies ${{ secrets.cfn-execution-role }}" + fi + + cdk bootstrap \ + aws://$(aws sts get-caller-identity --query Account --output text)/${{ inputs.aws-region }} \ + $role_args \ + $verbose + + echo "โœ… CDK environment bootstrapped successfully" + + # Synthesize CDK application + synth: + name: ๐Ÿ”จ CDK Synthesis + runs-on: ubuntu-latest + needs: [prepare, bootstrap] + if: always() && (needs.prepare.result == 'success' && (needs.bootstrap.result == 'success' || needs.bootstrap.result == 'skipped')) + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ secrets.cfn-execution-role }} + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Node.js dependencies..." + + verbose="" + if [ "${{ inputs.debug }}" = "true" ]; then + verbose="--verbose" + fi + + npm ci $verbose + + - name: Install or update CDK CLI + run: | + echo "๐Ÿ”ง Setting up AWS CDK CLI..." + + if [ -n "${{ inputs.cdk-version }}" ]; then + echo "๐Ÿ“Œ Installing CDK version: ${{ inputs.cdk-version }}" + npm install -g aws-cdk@${{ inputs.cdk-version }} + else + echo "๐Ÿ“ฆ Installing latest CDK version" + npm install -g aws-cdk + fi + + cdk --version + + - name: Synthesize CDK application + run: | + echo "๐Ÿ”จ Synthesizing CDK application..." + + verbose="" + if [ "${{ inputs.debug }}" = "true" ]; then + verbose="--verbose" + fi + + cdk synth ${{ inputs.cdk-stack-name }} \ + ${{ needs.prepare.outputs.context-args }} \ + $verbose + + echo "โœ… CDK synthesis completed successfully" + + - name: Upload synthesis artifacts + uses: actions/upload-artifact@v4 + with: + name: cdk-synthesis-${{ inputs.cdk-stack-name }} + path: cdk.out/ + retention-days: 30 + + # Generate deployment diff + diff: + name: ๐Ÿ“Š CDK Diff Analysis + runs-on: ubuntu-latest + needs: [prepare, synth] + if: always() && needs.synth.result == 'success' && inputs.destroy-mode == false + outputs: + has-changes: ${{ steps.diff-analysis.outputs.has-changes }} + diff-summary: ${{ steps.diff-analysis.outputs.summary }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ secrets.cfn-execution-role }} + + - name: Download synthesis artifacts + uses: actions/download-artifact@v4 + with: + name: cdk-synthesis-${{ inputs.cdk-stack-name }} + path: cdk.out/ + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Node.js dependencies..." + npm ci + + - name: Install CDK CLI + run: | + if [ -n "${{ inputs.cdk-version }}" ]; then + npm install -g aws-cdk@${{ inputs.cdk-version }} + else + npm install -g aws-cdk + fi + + - name: Generate deployment diff + id: diff-analysis + run: | + echo "๐Ÿ“Š Analyzing deployment changes..." + + # Generate diff output + diff_output=$(cdk diff ${{ inputs.cdk-stack-name }} \ + ${{ needs.prepare.outputs.context-args }} \ + --no-color 2>&1 || true) + + # Save diff to file for analysis + echo "$diff_output" > deployment-diff.txt + + # Analyze diff for changes + if echo "$diff_output" | grep -q "There were no differences"; then + echo "has-changes=false" >> $GITHUB_OUTPUT + echo "summary=No infrastructure changes detected" >> $GITHUB_OUTPUT + echo "โ„น๏ธ No changes detected in infrastructure" + else + echo "has-changes=true" >> $GITHUB_OUTPUT + + # Create summary + summary="Infrastructure changes detected" + if echo "$diff_output" | grep -q "Resources"; then + summary="$summary - Resource modifications found" + fi + + echo "summary=$summary" >> $GITHUB_OUTPUT + echo "โš ๏ธ Infrastructure changes detected!" + echo "$diff_output" + fi + + - name: Upload diff analysis + uses: actions/upload-artifact@v4 + with: + name: deployment-diff-${{ inputs.cdk-stack-name }} + path: deployment-diff.txt + retention-days: 30 + + # Manual approval for production deployments + approval: + name: ๐Ÿ” Production Approval + runs-on: ubuntu-latest + needs: [prepare, diff] + if: needs.prepare.outputs.requires-approval == 'true' && (needs.diff.result == 'success' || needs.diff.result == 'skipped') + environment: + name: ${{ inputs.environment-target }} + steps: + - name: Request deployment approval + run: | + echo "๐Ÿ” Manual approval required for production deployment" + echo "Stack: ${{ inputs.cdk-stack-name }}" + echo "Environment: ${{ inputs.environment-target }}" + echo "Region: ${{ inputs.aws-region }}" + + if [ "${{ needs.diff.outputs.has-changes }}" = "true" ]; then + echo "โš ๏ธ Infrastructure changes detected: ${{ needs.diff.outputs.diff-summary }}" + else + echo "โ„น๏ธ No infrastructure changes detected" + fi + + echo "โœ… Deployment approved - proceeding..." + + # Deploy or destroy CDK stack + deploy: + name: ${{ inputs.destroy-mode == true && '๐Ÿ’ฅ Destroy Stack' || '๐Ÿš€ Deploy Stack' }} + runs-on: ubuntu-latest + needs: [prepare, synth, approval, diff] + if: always() && needs.prepare.result == 'success' && needs.synth.result == 'success' && (needs.approval.result == 'success' || needs.approval.result == 'skipped') && (needs.diff.result == 'success' || needs.diff.result == 'skipped') + environment: ${{ inputs.environment-target }} + outputs: + stack-outputs: ${{ steps.deployment.outputs.stack-outputs }} + deployment-status: ${{ steps.deployment.outputs.status }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ secrets.cfn-execution-role }} + + - name: Download synthesis artifacts + if: inputs.destroy-mode == false + uses: actions/download-artifact@v4 + with: + name: cdk-synthesis-${{ inputs.cdk-stack-name }} + path: cdk.out/ + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Node.js dependencies..." + npm ci + + - name: Install CDK CLI + run: | + if [ -n "${{ inputs.cdk-version }}" ]; then + npm install -g aws-cdk@${{ inputs.cdk-version }} + else + npm install -g aws-cdk + fi + + - name: Execute CDK deployment + id: deployment + run: | + verbose="" + if [ "${{ inputs.debug }}" = "true" ]; then + verbose="--verbose" + fi + + if [ "${{ inputs.destroy-mode }}" = "true" ]; then + echo "๐Ÿ’ฅ Destroying CDK stack: ${{ inputs.cdk-stack-name }}" + + cdk destroy ${{ inputs.cdk-stack-name }} \ + ${{ needs.prepare.outputs.context-args }} \ + --force \ + $verbose + + echo "status=destroyed" >> $GITHUB_OUTPUT + echo "stack-outputs={}" >> $GITHUB_OUTPUT + echo "โœ… Stack destroyed successfully" + else + echo "๐Ÿš€ Deploying CDK stack: ${{ inputs.cdk-stack-name }}" + + cdk deploy ${{ inputs.cdk-stack-name }} \ + ${{ needs.prepare.outputs.context-args }} \ + --require-approval never \ + --outputs-file stack-outputs.json \ + $verbose + + # Extract stack outputs + if [ -f "stack-outputs.json" ]; then + outputs=$(cat stack-outputs.json) + echo "stack-outputs=$outputs" >> $GITHUB_OUTPUT + else + echo "stack-outputs={}" >> $GITHUB_OUTPUT + fi + + echo "status=success" >> $GITHUB_OUTPUT + echo "โœ… Stack deployed successfully" + fi + + - name: Upload deployment artifacts + if: inputs.destroy-mode == false && steps.deployment.outputs.status == 'success' + uses: actions/upload-artifact@v4 + with: + name: deployment-outputs-${{ inputs.cdk-stack-name }} + path: stack-outputs.json + retention-days: 30 + + # Post-deployment validation and drift detection + validate: + name: ๐Ÿ” Post-Deployment Validation + runs-on: ubuntu-latest + needs: [prepare, deploy] + if: needs.deploy.result == 'success' && inputs.destroy-mode == false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ secrets.cfn-execution-role }} + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Node.js dependencies..." + npm ci + + - name: Install CDK CLI + run: | + if [ -n "${{ inputs.cdk-version }}" ]; then + npm install -g aws-cdk@${{ inputs.cdk-version }} + else + npm install -g aws-cdk + fi + + - name: Validate stack deployment + run: | + echo "๐Ÿ” Validating deployed stack..." + + # Check stack status + stack_status=$(aws cloudformation describe-stacks \ + --stack-name ${{ inputs.cdk-stack-name }} \ + --query 'Stacks[0].StackStatus' \ + --output text) + + echo "Stack status: $stack_status" + + if [[ "$stack_status" =~ ^(CREATE_COMPLETE|UPDATE_COMPLETE)$ ]]; then + echo "โœ… Stack deployment validated successfully" + else + echo "โŒ Stack is in unexpected state: $stack_status" + exit 1 + fi + + - name: Check for stack drift + run: | + echo "๐Ÿ” Checking for infrastructure drift..." + + # Initiate drift detection + drift_id=$(aws cloudformation detect-stack-drift \ + --stack-name ${{ inputs.cdk-stack-name }} \ + --query 'StackDriftDetectionId' \ + --output text) + + echo "Drift detection initiated: $drift_id" + + # Wait for drift detection to complete + aws cloudformation wait stack-drift-detection-complete \ + --stack-drift-detection-id $drift_id + + # Get drift detection results + drift_status=$(aws cloudformation describe-stack-drift-detection-status \ + --stack-drift-detection-id $drift_id \ + --query 'StackDriftStatus' \ + --output text) + + echo "Stack drift status: $drift_status" + + case $drift_status in + "IN_SYNC") + echo "โœ… No infrastructure drift detected" + ;; + "DRIFTED") + echo "โš ๏ธ Infrastructure drift detected - manual review recommended" + ;; + *) + echo "โ“ Unknown drift status: $drift_status" + ;; + esac + + - name: Display deployment summary + run: | + echo "๐Ÿ“‹ Deployment Summary" + echo "====================" + echo "Stack Name: ${{ inputs.cdk-stack-name }}" + echo "Environment: ${{ inputs.environment-target }}" + echo "Region: ${{ inputs.aws-region }}" + echo "Status: ${{ needs.deploy.outputs.deployment-status }}" + echo "Node Version: ${{ inputs.node-version }}" + + if [ -n "${{ inputs.cdk-version }}" ]; then + echo "CDK Version: ${{ inputs.cdk-version }}" + fi + + echo "" + echo "๐ŸŽ‰ Deployment completed successfully!" \ No newline at end of file diff --git a/.github/workflows/bigcommerce-theme-deploy.yml b/.github/workflows/bigcommerce-theme-deploy.yml new file mode 100644 index 0000000..79efa4d --- /dev/null +++ b/.github/workflows/bigcommerce-theme-deploy.yml @@ -0,0 +1,639 @@ +name: ๐Ÿ›๏ธ BigCommerce Theme Deploy + +# BigCommerce Stencil theme deployment workflow +# Supports theme bundling, validation, environment promotion, and rollback capabilities +# Includes asset optimization, configuration backup, and multi-environment deployment +on: + workflow_call: + inputs: + # Core Configuration + store-hash: + description: "BigCommerce store hash (required)" + type: string + required: true + environment: + description: "Target environment (staging/production)" + type: string + required: false + default: "staging" + theme-name: + description: "Theme name for identification (required)" + type: string + required: true + + # Deployment Control + activate-theme: + description: "Activate theme after deployment" + type: boolean + required: false + default: true + bundle-optimization: + description: "Enable theme bundle optimization" + type: boolean + required: false + default: true + backup-current: + description: "Backup current theme before deployment" + type: boolean + required: false + default: true + + # Technical Configuration + node-version: + description: "Node.js version for Stencil CLI" + type: string + required: false + default: "18" + stencil-version: + description: "Stencil CLI version (optional, for pinning)" + type: string + required: false + theme-config: + description: "Theme configuration as JSON (optional)" + type: string + required: false + + # Advanced Options + variation-name: + description: "Theme variation to activate (optional)" + type: string + required: false + channel-ids: + description: "Channel IDs to apply theme (comma-separated, optional)" + type: string + required: false + apply-to-all-channels: + description: "Apply theme to all channels" + type: boolean + required: false + default: false + delete-oldest: + description: "Delete oldest theme to make room" + type: boolean + required: false + default: false + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + + secrets: + bigcommerce-access-token: + description: "BigCommerce API access token" + required: true + bigcommerce-client-id: + description: "BigCommerce API client ID" + required: true + bigcommerce-client-secret: + description: "BigCommerce API client secret" + required: true + + outputs: + theme-uuid: + description: "Deployed theme UUID" + value: ${{ jobs.deploy.outputs.theme-uuid }} + theme-version: + description: "Deployed theme version" + value: ${{ jobs.deploy.outputs.theme-version }} + deployment-url: + description: "Theme preview URL" + value: ${{ jobs.deploy.outputs.deployment-url }} + backup-created: + description: "Whether backup was created" + value: ${{ jobs.deploy.outputs.backup-created }} + +jobs: + validate-environment: + name: ๐Ÿ” Validate Environment + runs-on: ubuntu-latest + outputs: + store-url: ${{ steps.validate.outputs.store-url }} + environment-validated: ${{ steps.validate.outputs.environment-validated }} + steps: + - name: Validate inputs + id: validate + run: | + # Validate store hash format + if [[ ! "${{ inputs.store-hash }}" =~ ^[a-z0-9]{10}$ ]]; then + echo "โŒ Invalid store hash format. Expected 10 character alphanumeric string." + exit 1 + fi + + # Validate environment + if [[ ! "${{ inputs.environment }}" =~ ^(staging|production)$ ]]; then + echo "โŒ Invalid environment. Must be 'staging' or 'production'." + exit 1 + fi + + # Set store URL + store_url="https://${{ inputs.store-hash }}.mybigcommerce.com" + echo "store-url=$store_url" >> $GITHUB_OUTPUT + echo "environment-validated=true" >> $GITHUB_OUTPUT + + echo "โœ… Environment validation passed" + echo "๐Ÿช Store URL: $store_url" + echo "๐ŸŽฏ Environment: ${{ inputs.environment }}" + + setup-theme: + name: ๐Ÿ”ง Setup Theme Environment + runs-on: ubuntu-latest + needs: validate-environment + outputs: + stencil-version: ${{ steps.setup.outputs.stencil-version }} + package-manager: ${{ steps.setup.outputs.package-manager }} + steps: + - name: Checkout theme code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for theme versioning + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Setup Stencil CLI + id: setup + run: | + # Determine package manager + if [ -f "yarn.lock" ]; then + package_manager="yarn" + echo "๐Ÿ“ฆ Using Yarn package manager" + else + package_manager="npm" + echo "๐Ÿ“ฆ Using NPM package manager" + fi + + # Install Stencil CLI + if [ -n "${{ inputs.stencil-version }}" ]; then + stencil_version="${{ inputs.stencil-version }}" + npm install -g @bigcommerce/stencil-cli@$stencil_version + echo "๐Ÿ”ง Installed Stencil CLI version: $stencil_version" + else + npm install -g @bigcommerce/stencil-cli + stencil_version=$(stencil --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + echo "๐Ÿ”ง Installed latest Stencil CLI version: $stencil_version" + fi + + # Verify installation + stencil --version + + # Output variables + echo "stencil-version=$stencil_version" >> $GITHUB_OUTPUT + echo "package-manager=$package_manager" >> $GITHUB_OUTPUT + + - name: Install theme dependencies + run: | + if [ "${{ steps.setup.outputs.package-manager }}" = "yarn" ]; then + yarn install --frozen-lockfile + else + npm ci + fi + + echo "โœ… Theme dependencies installed" + + - name: Cache theme setup + uses: actions/cache@v3 + with: + path: | + ~/.npm + ~/.cache/yarn + node_modules + key: theme-${{ runner.os }}-node${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + restore-keys: | + theme-${{ runner.os }}-node${{ inputs.node-version }}- + + backup-current-theme: + name: ๐Ÿ’พ Backup Current Theme + runs-on: ubuntu-latest + needs: [validate-environment, setup-theme] + if: inputs.backup-current == true + outputs: + backup-created: ${{ steps.backup.outputs.backup-created }} + backup-theme-uuid: ${{ steps.backup.outputs.backup-theme-uuid }} + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install Stencil CLI + run: | + if [ -n "${{ inputs.stencil-version }}" ]; then + npm install -g @bigcommerce/stencil-cli@${{ inputs.stencil-version }} + else + npm install -g @bigcommerce/stencil-cli + fi + + - name: Configure Stencil + run: | + # Create Stencil configuration + cat > .stencil << EOF + { + "normalStoreUrl": "${{ needs.validate-environment.outputs.store-url }}", + "accessToken": "${{ secrets.bigcommerce-access-token }}", + "port": 3000 + } + EOF + + echo "๐Ÿ”ง Stencil configuration created" + + - name: Backup current theme + id: backup + run: | + timestamp=$(date +%Y%m%d_%H%M%S) + backup_name="backup_${{ inputs.theme-name }}_${timestamp}" + + echo "๐Ÿ’พ Creating backup: $backup_name" + + # Pull current theme configuration + if stencil pull; then + echo "โœ… Current theme configuration pulled successfully" + + # Store backup information + echo "backup-created=true" >> $GITHUB_OUTPUT + echo "backup-theme-uuid=$backup_name" >> $GITHUB_OUTPUT + + echo "๐Ÿ’พ Backup created successfully: $backup_name" + else + echo "โš ๏ธ Could not pull current theme configuration - continuing without backup" + echo "backup-created=false" >> $GITHUB_OUTPUT + fi + + validate-theme: + name: โœ… Validate Theme + runs-on: ubuntu-latest + needs: [setup-theme] + steps: + - name: Checkout theme code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Restore theme cache + uses: actions/cache@v3 + with: + path: | + ~/.npm + ~/.cache/yarn + node_modules + key: theme-${{ runner.os }}-node${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + + - name: Install dependencies + run: | + if [ "${{ needs.setup-theme.outputs.package-manager }}" = "yarn" ]; then + yarn install + else + npm ci + fi + + - name: Validate theme structure + run: | + echo "๐Ÿ” Validating theme structure..." + + # Check required files + required_files=("config.json" "schema.json") + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + echo "โŒ Missing required file: $file" + exit 1 + fi + echo "โœ… Found required file: $file" + done + + # Check required directories + required_dirs=("templates" "assets") + for dir in "${required_dirs[@]}"; do + if [ ! -d "$dir" ]; then + echo "โŒ Missing required directory: $dir" + exit 1 + fi + echo "โœ… Found required directory: $dir" + done + + - name: Check file permissions + run: | + echo "๐Ÿ” Checking file permissions..." + + # Find files with incorrect permissions + incorrect_files=$(find . -type f ! -perm 644 -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null || true) + incorrect_dirs=$(find . -type d ! -perm 755 -not -path "./.git" -not -path "./.git/*" -not -path "./node_modules" -not -path "./node_modules/*" 2>/dev/null || true) + + if [ -n "$incorrect_files" ] || [ -n "$incorrect_dirs" ]; then + echo "โš ๏ธ Found files/directories with incorrect permissions:" + echo "$incorrect_files" + echo "$incorrect_dirs" + echo "๐Ÿ”ง Fixing permissions..." + + # Fix permissions + find . -type f -not -path "./.git/*" -not -path "./node_modules/*" -exec chmod 644 {} \; 2>/dev/null || true + find . -type d -not -path "./.git" -not -path "./.git/*" -not -path "./node_modules" -not -path "./node_modules/*" -exec chmod 755 {} \; 2>/dev/null || true + + echo "โœ… File permissions fixed" + else + echo "โœ… All file permissions are correct" + fi + + - name: Validate JSON files + run: | + echo "๐Ÿ” Validating JSON configuration files..." + + json_files=("config.json" "schema.json") + for file in "${json_files[@]}"; do + if ! jq empty "$file" 2>/dev/null; then + echo "โŒ Invalid JSON in file: $file" + exit 1 + fi + echo "โœ… Valid JSON: $file" + done + + optimize-theme: + name: โšก Optimize Theme + runs-on: ubuntu-latest + needs: [setup-theme, validate-theme] + if: inputs.bundle-optimization == true + steps: + - name: Checkout theme code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Restore theme cache + uses: actions/cache@v3 + with: + path: | + ~/.npm + ~/.cache/yarn + node_modules + key: theme-${{ runner.os }}-node${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + + - name: Install dependencies + run: | + if [ "${{ needs.setup-theme.outputs.package-manager }}" = "yarn" ]; then + yarn install + else + npm ci + fi + + - name: Optimize assets + run: | + echo "โšก Optimizing theme assets..." + + # Run build process if available + if [ "${{ needs.setup-theme.outputs.package-manager }}" = "yarn" ]; then + if yarn run --list 2>/dev/null | grep -q "build"; then + echo "๐Ÿ—๏ธ Running theme build process..." + yarn run build + fi + if yarn run --list 2>/dev/null | grep -q "optimize"; then + echo "โšก Running theme optimization..." + yarn run optimize + fi + else + if npm run build 2>/dev/null; then + echo "๐Ÿ—๏ธ Theme build completed" + fi + if npm run optimize 2>/dev/null; then + echo "โšก Theme optimization completed" + fi + fi + + echo "โœ… Asset optimization completed" + + - name: Upload optimized theme + uses: actions/upload-artifact@v4 + with: + name: optimized-theme + path: | + . + !node_modules + !.git + !*.log + retention-days: 1 + + deploy: + name: ๐Ÿš€ Deploy Theme + runs-on: ubuntu-latest + needs: [validate-environment, setup-theme, backup-current-theme, validate-theme, optimize-theme] + if: always() && (needs.validate-environment.result == 'success' && needs.setup-theme.result == 'success' && needs.validate-theme.result == 'success') + outputs: + theme-uuid: ${{ steps.deploy.outputs.theme-uuid }} + theme-version: ${{ steps.deploy.outputs.theme-version }} + deployment-url: ${{ steps.deploy.outputs.deployment-url }} + backup-created: ${{ needs.backup-current-theme.outputs.backup-created || 'false' }} + steps: + - name: Checkout theme code + if: needs.optimize-theme.result != 'success' + uses: actions/checkout@v4 + + - name: Download optimized theme + if: needs.optimize-theme.result == 'success' + uses: actions/download-artifact@v4 + with: + name: optimized-theme + path: . + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install Stencil CLI + run: | + if [ -n "${{ inputs.stencil-version }}" ]; then + npm install -g @bigcommerce/stencil-cli@${{ inputs.stencil-version }} + else + npm install -g @bigcommerce/stencil-cli + fi + + echo "๐Ÿ”ง Stencil CLI installed: $(stencil --version)" + + - name: Configure Stencil + run: | + # Create Stencil configuration + cat > .stencil << EOF + { + "normalStoreUrl": "${{ needs.validate-environment.outputs.store-url }}", + "accessToken": "${{ secrets.bigcommerce-access-token }}", + "port": 3000 + } + EOF + + # Apply custom theme configuration if provided + if [ -n "${{ inputs.theme-config }}" ]; then + echo "๐Ÿ”ง Applying custom theme configuration..." + echo '${{ inputs.theme-config }}' | jq . > custom-config.json + + # Merge with existing config.json + if [ -f "config.json" ]; then + jq -s '.[0] * .[1]' config.json custom-config.json > merged-config.json + mv merged-config.json config.json + echo "โœ… Custom configuration applied" + fi + + rm -f custom-config.json + fi + + echo "๐Ÿ”ง Stencil configuration completed" + + - name: Bundle theme + run: | + echo "๐Ÿ“ฆ Creating theme bundle..." + + if [ "${{ inputs.debug }}" = "true" ]; then + stencil bundle --debug + else + stencil bundle + fi + + # Check bundle size + bundle_file=$(find . -name "*.zip" -type f | head -1) + if [ -n "$bundle_file" ]; then + bundle_size=$(stat -c%s "$bundle_file") + bundle_size_mb=$((bundle_size / 1024 / 1024)) + + echo "๐Ÿ“ฆ Bundle created: $bundle_file" + echo "๐Ÿ“ Bundle size: ${bundle_size_mb}MB" + + if [ $bundle_size_mb -gt 50 ]; then + echo "โŒ Bundle size exceeds 50MB limit" + exit 1 + fi + else + echo "โŒ Bundle creation failed - no .zip file found" + exit 1 + fi + + - name: Deploy theme to BigCommerce + id: deploy + run: | + echo "๐Ÿš€ Deploying theme to BigCommerce..." + + # Build stencil push command + push_cmd="stencil push" + + # Add activation flag + if [ "${{ inputs.activate-theme }}" = "true" ]; then + if [ -n "${{ inputs.variation-name }}" ]; then + push_cmd="$push_cmd -a ${{ inputs.variation-name }}" + else + push_cmd="$push_cmd -a" + fi + fi + + # Add channel configuration + if [ "${{ inputs.apply-to-all-channels }}" = "true" ]; then + push_cmd="$push_cmd -allc" + elif [ -n "${{ inputs.channel-ids }}" ]; then + # Convert comma-separated to space-separated + channel_list=$(echo "${{ inputs.channel-ids }}" | tr ',' ' ') + push_cmd="$push_cmd -c $channel_list" + fi + + # Add delete oldest flag + if [ "${{ inputs.delete-oldest }}" = "true" ]; then + push_cmd="$push_cmd -d" + fi + + echo "๐Ÿ“ค Executing: $push_cmd" + + # Execute deployment with error handling + if [ "${{ inputs.debug }}" = "true" ]; then + set -x + fi + + # Capture output for parsing + output_file=$(mktemp) + if eval "$push_cmd" > "$output_file" 2>&1; then + echo "โœ… Theme deployment successful!" + + # Parse deployment output for theme information + theme_uuid=$(grep -o 'Theme ID: [a-f0-9-]*' "$output_file" | cut -d' ' -f3 || echo "unknown") + theme_version=$(date +%Y%m%d_%H%M%S) + deployment_url="${{ needs.validate-environment.outputs.store-url }}" + + echo "๐ŸŽฏ Theme UUID: $theme_uuid" + echo "๐Ÿ“‹ Theme Version: $theme_version" + echo "๐ŸŒ Deployment URL: $deployment_url" + + # Set outputs + echo "theme-uuid=$theme_uuid" >> $GITHUB_OUTPUT + echo "theme-version=$theme_version" >> $GITHUB_OUTPUT + echo "deployment-url=$deployment_url" >> $GITHUB_OUTPUT + + cat "$output_file" + else + echo "โŒ Theme deployment failed!" + cat "$output_file" + exit 1 + fi + + rm -f "$output_file" + + verify-deployment: + name: ๐Ÿ” Verify Deployment + runs-on: ubuntu-latest + needs: [deploy] + steps: + - name: Verify theme deployment + run: | + echo "๐Ÿ” Verifying theme deployment..." + + deployment_url="${{ needs.deploy.outputs.deployment-url }}" + theme_uuid="${{ needs.deploy.outputs.theme-uuid }}" + + echo "๐ŸŒ Store URL: $deployment_url" + echo "๐ŸŽฏ Theme UUID: $theme_uuid" + + # Basic connectivity check + if curl -sSf "$deployment_url" > /dev/null; then + echo "โœ… Store is accessible" + else + echo "โš ๏ธ Store accessibility check failed - this may be normal for private stores" + fi + + echo "โœ… Deployment verification completed" + + report-deployment: + name: ๐Ÿ“Š Deployment Report + runs-on: ubuntu-latest + needs: [deploy, verify-deployment] + if: always() + steps: + - name: Generate deployment report + run: | + echo "# ๐Ÿ›๏ธ BigCommerce Theme Deployment Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Theme Name**: ${{ inputs.theme-name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Environment**: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY + echo "- **Store Hash**: ${{ inputs.store-hash }}" >> $GITHUB_STEP_SUMMARY + echo "- **Theme UUID**: ${{ needs.deploy.outputs.theme-uuid }}" >> $GITHUB_STEP_SUMMARY + echo "- **Theme Version**: ${{ needs.deploy.outputs.theme-version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Deployment URL**: ${{ needs.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "## Configuration" >> $GITHUB_STEP_SUMMARY + echo "- **Node Version**: ${{ inputs.node-version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Stencil Version**: ${{ needs.setup-theme.outputs.stencil-version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Theme Activated**: ${{ inputs.activate-theme }}" >> $GITHUB_STEP_SUMMARY + echo "- **Bundle Optimized**: ${{ inputs.bundle-optimization }}" >> $GITHUB_STEP_SUMMARY + echo "- **Backup Created**: ${{ needs.deploy.outputs.backup-created }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Deployment status + if [ "${{ needs.deploy.result }}" = "success" ]; then + echo "## โœ… Deployment Status: SUCCESS" >> $GITHUB_STEP_SUMMARY + echo "Theme has been successfully deployed to BigCommerce!" >> $GITHUB_STEP_SUMMARY + else + echo "## โŒ Deployment Status: FAILED" >> $GITHUB_STEP_SUMMARY + echo "Theme deployment encountered issues. Check the logs above for details." >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/cli-tools-release.yml b/.github/workflows/cli-tools-release.yml new file mode 100644 index 0000000..37a2729 --- /dev/null +++ b/.github/workflows/cli-tools-release.yml @@ -0,0 +1,854 @@ +name: ๐Ÿ› ๏ธ CLI Tools Release + +on: + workflow_call: + inputs: + # Core Configuration + tool-name: + description: "CLI tool name (required)" + type: string + required: true + version-strategy: + description: "Version strategy (semantic/tag/custom)" + type: string + required: false + default: "semantic" + platforms: + description: "Target platforms for compilation" + type: string + required: false + default: "linux-x64,darwin-x64,darwin-arm64" + + # Distribution Configuration + s3-bucket: + description: "S3 bucket for distribution (required)" + type: string + required: true + s3-path: + description: "S3 path prefix for distribution" + type: string + required: false + default: "" + homebrew-tap: + description: "Homebrew tap repository (optional, format: owner/repo)" + type: string + required: false + default: "" + + # Security Configuration + gpg-sign: + description: "Enable GPG signing for release integrity" + type: boolean + required: false + default: true + + # Release Configuration + create-release: + description: "Create GitHub release" + type: boolean + required: false + default: true + release-notes: + description: "Custom release notes (optional)" + type: string + required: false + default: "" + + # Technical Configuration + node-version: + description: "Node.js version for OCLIF" + type: string + required: false + default: "18" + oclif-version: + description: "Pin specific OCLIF version (optional)" + type: string + required: false + default: "" + + # Advanced Configuration + aws-region: + description: "AWS region for S3 distribution" + type: string + required: false + default: "ap-southeast-2" + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + + secrets: + aws-access-key-id: + description: "AWS access key ID for S3 distribution" + required: true + aws-secret-access-key: + description: "AWS secret access key for S3 distribution" + required: true + gpg-private-key: + description: "GPG private key for signing (optional but recommended)" + required: false + gpg-passphrase: + description: "GPG passphrase for signing (optional but recommended)" + required: false + homebrew-github-token: + description: "GitHub token for Homebrew tap updates (optional)" + required: false + + outputs: + release-version: + description: "Released version identifier" + value: ${{ jobs.release.outputs.version }} + release-url: + description: "GitHub release URL" + value: ${{ jobs.release.outputs.url }} + distribution-urls: + description: "Distribution URLs as JSON object" + value: ${{ jobs.distribute.outputs.urls }} + homebrew-formula: + description: "Homebrew formula update status" + value: ${{ jobs.homebrew.outputs.status }} + +jobs: + # Validate inputs and prepare release configuration + prepare: + name: ๐Ÿ” Prepare CLI Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + platforms: ${{ steps.platforms.outputs.validated }} + release-name: ${{ steps.metadata.outputs.name }} + release-tag: ${{ steps.metadata.outputs.tag }} + should-sign: ${{ steps.signing.outputs.enabled }} + steps: + - name: Validate required inputs + run: | + echo "๐Ÿ” Validating CLI release configuration..." + + if [ -z "${{ inputs.tool-name }}" ]; then + echo "โŒ Error: tool-name is required" + exit 1 + fi + + if [ -z "${{ inputs.s3-bucket }}" ]; then + echo "โŒ Error: s3-bucket is required for distribution" + exit 1 + fi + + # Validate version strategy + case "${{ inputs.version-strategy }}" in + semantic|tag|custom) + echo "โœ… Version strategy: ${{ inputs.version-strategy }}" + ;; + *) + echo "โŒ Error: version-strategy must be one of: semantic, tag, custom" + exit 1 + ;; + esac + + # Validate tool name format (CLI-friendly) + if [[ ! "${{ inputs.tool-name }}" =~ ^[a-zA-Z0-9-_]+$ ]]; then + echo "โŒ Error: tool-name must contain only alphanumeric characters, hyphens, and underscores" + exit 1 + fi + + echo "โœ… All inputs validated successfully" + + - name: Determine version + id: version + run: | + echo "๐Ÿ”ข Determining release version..." + + version="" + case "${{ inputs.version-strategy }}" in + semantic|tag) + if [ "${{ github.ref_type }}" = "tag" ]; then + version="${{ github.ref_name }}" + # Remove 'v' prefix if present for consistency + version=$(echo "$version" | sed 's/^v//') + else + echo "โŒ Error: semantic/tag strategy requires a git tag" + exit 1 + fi + ;; + custom) + # Use timestamp-based version for custom builds + version="$(date +%Y.%m.%d)-$(echo "${{ github.sha }}" | cut -c1-7)" + ;; + esac + + # Validate semantic version format for semantic strategy + if [ "${{ inputs.version-strategy }}" = "semantic" ]; then + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then + echo "โŒ Error: semantic version must follow semver format (x.y.z)" + exit 1 + fi + fi + + echo "version=$version" >> $GITHUB_OUTPUT + echo "โœ… Release version: $version" + + - name: Validate platforms + id: platforms + run: | + echo "๐Ÿ—๏ธ Validating compilation platforms..." + + platforms="${{ inputs.platforms }}" + validated_platforms="" + + # Split platforms and validate each + IFS=',' read -ra PLATFORM_LIST <<< "$platforms" + for platform in "${PLATFORM_LIST[@]}"; do + platform=$(echo "$platform" | xargs) # trim whitespace + case "$platform" in + linux-x64|linux-arm64|darwin-x64|darwin-arm64|win32-x64|win32-arm64) + echo "โœ… Platform supported: $platform" + if [ -n "$validated_platforms" ]; then + validated_platforms="$validated_platforms,$platform" + else + validated_platforms="$platform" + fi + ;; + *) + echo "โš ๏ธ Warning: Unusual platform specified: $platform" + if [ -n "$validated_platforms" ]; then + validated_platforms="$validated_platforms,$platform" + else + validated_platforms="$platform" + fi + ;; + esac + done + + echo "validated=$validated_platforms" >> $GITHUB_OUTPUT + echo "โœ… Platforms configured: $validated_platforms" + + - name: Configure release metadata + id: metadata + run: | + echo "๐Ÿ“‹ Configuring release metadata..." + + version="${{ steps.version.outputs.version }}" + tool_name="${{ inputs.tool-name }}" + + release_name="$tool_name v$version" + release_tag="v$version" + + echo "name=$release_name" >> $GITHUB_OUTPUT + echo "tag=$release_tag" >> $GITHUB_OUTPUT + + echo "โœ… Release metadata configured" + echo "๐Ÿ“ฆ Release name: $release_name" + echo "๐Ÿท๏ธ Release tag: $release_tag" + + - name: Configure signing + id: signing + run: | + echo "๐Ÿ” Configuring GPG signing..." + + should_sign="false" + if [ "${{ inputs.gpg-sign }}" = "true" ]; then + if [ -n "${{ secrets.gpg-private-key }}" ]; then + should_sign="true" + echo "โœ… GPG signing enabled" + else + echo "โš ๏ธ Warning: GPG signing requested but gpg-private-key not provided" + echo "โ„น๏ธ Proceeding without signing" + fi + else + echo "โ„น๏ธ GPG signing disabled" + fi + + echo "enabled=$should_sign" >> $GITHUB_OUTPUT + + # Build and package CLI tools for multiple platforms + build: + name: ๐Ÿ—๏ธ Build CLI Packages + runs-on: ubuntu-latest + needs: prepare + outputs: + artifacts: ${{ steps.package.outputs.artifacts }} + manifest: ${{ steps.manifest.outputs.content }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing CLI dependencies..." + + if [ "${{ inputs.debug }}" = "true" ]; then + npm ci --verbose + else + npm ci + fi + + # Install or pin OCLIF version if specified + if [ -n "${{ inputs.oclif-version }}" ]; then + echo "๐Ÿ“Œ Installing OCLIF version: ${{ inputs.oclif-version }}" + npm install --save-dev @oclif/core@${{ inputs.oclif-version }} + fi + + echo "โœ… Dependencies installed" + + - name: Run tests + run: | + echo "๐Ÿงช Running test suite..." + + if npm run test --if-present; then + echo "โœ… Tests passed" + else + echo "โš ๏ธ Tests failed or not configured" + if [ "${{ inputs.debug }}" = "true" ]; then + echo "Debug mode: Continuing despite test failures" + else + exit 1 + fi + fi + + - name: Build CLI application + run: | + echo "๐Ÿ”จ Building CLI application..." + + # Run build script if available + if npm run build --if-present; then + echo "โœ… Build completed" + else + echo "โ„น๏ธ No build script found, proceeding with packaging" + fi + + - name: Package for multiple platforms + id: package + run: | + echo "๐Ÿ“ฆ Packaging CLI for multiple platforms..." + + version="${{ needs.prepare.outputs.version }}" + tool_name="${{ inputs.tool-name }}" + platforms="${{ needs.prepare.outputs.platforms }}" + + # Create dist directory + mkdir -p dist + + artifacts="" + + # Split platforms and build each + IFS=',' read -ra PLATFORM_LIST <<< "$platforms" + for platform in "${PLATFORM_LIST[@]}"; do + platform=$(echo "$platform" | xargs) # trim whitespace + echo "๐Ÿ—๏ธ Building for platform: $platform" + + # Use OCLIF to package for each platform + if command -v npx oclif pack >/dev/null 2>&1; then + npx oclif pack --platform=$platform --output=dist/ + else + echo "โš ๏ธ OCLIF not available, using alternative packaging" + # Alternative packaging strategy + package_name="${tool_name}-v${version}-${platform}" + + # Create platform-specific package + mkdir -p "dist/$package_name" + cp -r . "dist/$package_name/" 2>/dev/null || true + + # Create tarball + cd dist + tar -czf "${package_name}.tar.gz" "$package_name/" + rm -rf "$package_name" + cd .. + fi + + # Track artifacts + if [ -n "$artifacts" ]; then + artifacts="$artifacts,${platform}" + else + artifacts="${platform}" + fi + done + + echo "artifacts=$artifacts" >> $GITHUB_OUTPUT + echo "โœ… Packaging completed for platforms: $artifacts" + + # List generated files + echo "๐Ÿ“ Generated artifacts:" + ls -la dist/ + + - name: Generate update manifest + id: manifest + run: | + echo "๐Ÿ“„ Generating update manifest..." + + version="${{ needs.prepare.outputs.version }}" + tool_name="${{ inputs.tool-name }}" + platforms="${{ needs.prepare.outputs.platforms }}" + + # Create manifest for self-updating CLIs + manifest='{ + "version": "'$version'", + "name": "'$tool_name'", + "published": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", + "platforms": {} + }' + + # Add platform-specific information + IFS=',' read -ra PLATFORM_LIST <<< "$platforms" + for platform in "${PLATFORM_LIST[@]}"; do + platform=$(echo "$platform" | xargs) + + # Find corresponding artifact + artifact_file=$(find dist/ -name "*${platform}*" -type f | head -1) + if [ -n "$artifact_file" ]; then + file_size=$(stat -c%s "$artifact_file" 2>/dev/null || stat -f%z "$artifact_file" 2>/dev/null || echo "0") + file_hash=$(sha256sum "$artifact_file" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$artifact_file" | cut -d' ' -f1) + + # Update manifest with platform info + manifest=$(echo "$manifest" | jq --arg platform "$platform" \ + --arg size "$file_size" \ + --arg hash "$file_hash" \ + --arg filename "$(basename "$artifact_file")" \ + '.platforms[$platform] = { + "filename": $filename, + "size": ($size | tonumber), + "sha256": $hash + }') + fi + done + + # Save manifest + echo "$manifest" | jq . > dist/manifest.json + + # Output for later use + manifest_content=$(echo "$manifest" | jq -c .) + echo "content=$manifest_content" >> $GITHUB_OUTPUT + + echo "โœ… Update manifest generated" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: cli-packages + path: dist/ + retention-days: 30 + + # Sign packages if GPG signing is enabled + sign: + name: ๐Ÿ” Sign CLI Packages + runs-on: ubuntu-latest + needs: [prepare, build] + if: needs.prepare.outputs.should-sign == 'true' + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: cli-packages + path: dist/ + + - name: Setup GPG + run: | + echo "๐Ÿ” Setting up GPG signing..." + + # Import GPG private key + echo "${{ secrets.gpg-private-key }}" | gpg --batch --import + + # Configure GPG for non-interactive use + echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf + echo "use-agent" >> ~/.gnupg/gpg.conf + + echo "โœ… GPG configured" + + - name: Sign packages + run: | + echo "โœ๏ธ Signing CLI packages..." + + cd dist + + # Sign all package files + for file in *.tar.gz *.zip; do + if [ -f "$file" ]; then + echo "Signing: $file" + + if [ -n "${{ secrets.gpg-passphrase }}" ]; then + echo "${{ secrets.gpg-passphrase }}" | gpg --batch --yes --passphrase-fd 0 \ + --detach-sign --armor "$file" + else + gpg --batch --yes --detach-sign --armor "$file" + fi + + echo "โœ… Signed: $file" + fi + done + + # Sign manifest + if [ -f "manifest.json" ]; then + echo "Signing manifest.json" + if [ -n "${{ secrets.gpg-passphrase }}" ]; then + echo "${{ secrets.gpg-passphrase }}" | gpg --batch --yes --passphrase-fd 0 \ + --detach-sign --armor "manifest.json" + else + gpg --batch --yes --detach-sign --armor "manifest.json" + fi + fi + + echo "โœ… All packages signed" + + - name: Upload signed artifacts + uses: actions/upload-artifact@v4 + with: + name: cli-packages-signed + path: dist/ + retention-days: 30 + + # Distribute packages to S3 + distribute: + name: ๐Ÿ“ฆ Distribute CLI Packages + runs-on: ubuntu-latest + needs: [prepare, build, sign] + if: always() && needs.build.result == 'success' && (needs.sign.result == 'success' || needs.sign.result == 'skipped') + outputs: + urls: ${{ steps.upload.outputs.urls }} + manifest-url: ${{ steps.upload.outputs.manifest-url }} + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ${{ needs.prepare.outputs.should-sign == 'true' && 'cli-packages-signed' || 'cli-packages' }} + path: dist/ + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Upload to S3 + id: upload + run: | + echo "โ˜๏ธ Uploading CLI packages to S3..." + + bucket="${{ inputs.s3-bucket }}" + s3_path="${{ inputs.s3-path }}" + tool_name="${{ inputs.tool-name }}" + version="${{ needs.prepare.outputs.version }}" + + # Construct S3 paths + if [ -n "$s3_path" ]; then + base_path="s3://$bucket/$s3_path/$tool_name/$version" + latest_path="s3://$bucket/$s3_path/$tool_name/latest" + else + base_path="s3://$bucket/$tool_name/$version" + latest_path="s3://$bucket/$tool_name/latest" + fi + + echo "๐Ÿ“ Uploading to: $base_path" + + cd dist + + # Upload versioned files + aws s3 sync . "$base_path/" --exclude "*" --include "*.tar.gz" --include "*.zip" --include "*.asc" --include "manifest.json" + + # Upload to latest (for auto-updates) + aws s3 sync . "$latest_path/" --exclude "*" --include "*.tar.gz" --include "*.zip" --include "*.asc" --include "manifest.json" + + # Generate distribution URLs + urls='{' + manifest_url="" + + for file in *.tar.gz *.zip; do + if [ -f "$file" ]; then + if [ -n "$s3_path" ]; then + url="https://$bucket.s3.${{ inputs.aws-region }}.amazonaws.com/$s3_path/$tool_name/$version/$file" + else + url="https://$bucket.s3.${{ inputs.aws-region }}.amazonaws.com/$tool_name/$version/$file" + fi + + platform=$(echo "$file" | sed -E 's/.*-v[0-9.]+-([^.]+)\.(tar\.gz|zip)$/\1/') + urls=$(echo "$urls" | jq --arg platform "$platform" --arg url "$url" '. + {($platform): $url}') + fi + done + + # Add manifest URL + if [ -f "manifest.json" ]; then + if [ -n "$s3_path" ]; then + manifest_url="https://$bucket.s3.${{ inputs.aws-region }}.amazonaws.com/$s3_path/$tool_name/latest/manifest.json" + else + manifest_url="https://$bucket.s3.${{ inputs.aws-region }}.amazonaws.com/$tool_name/latest/manifest.json" + fi + fi + + urls=$(echo "$urls" | jq --arg manifest "$manifest_url" '. + {"manifest": $manifest}') + urls="$urls}" + + echo "urls=$urls" >> $GITHUB_OUTPUT + echo "manifest-url=$manifest_url" >> $GITHUB_OUTPUT + + echo "โœ… Distribution completed" + echo "๐Ÿ“Š Distribution URLs:" + echo "$urls" | jq . + + # Create GitHub release + release: + name: ๐Ÿš€ Create GitHub Release + runs-on: ubuntu-latest + needs: [prepare, build, distribute] + if: inputs.create-release == true && needs.distribute.result == 'success' + outputs: + version: ${{ needs.prepare.outputs.version }} + url: ${{ steps.create-release.outputs.html_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ${{ needs.prepare.outputs.should-sign == 'true' && 'cli-packages-signed' || 'cli-packages' }} + path: dist/ + + - name: Generate release notes + id: release-notes + run: | + echo "๐Ÿ“ Generating release notes..." + + version="${{ needs.prepare.outputs.version }}" + tool_name="${{ inputs.tool-name }}" + + # Start with custom release notes if provided + if [ -n "${{ inputs.release-notes }}" ]; then + base_notes="${{ inputs.release-notes }}" + else + base_notes="Release of $tool_name version $version" + fi + + # Create release notes using heredoc + cat > release_notes.md << EOF + $base_notes + + ## Supported Platforms + ${{ needs.prepare.outputs.platforms }} + + ## Installation + + ### Download directly: + EOF + + # Add download links + urls='${{ needs.distribute.outputs.urls }}' + echo "$urls" | jq -r 'to_entries[] | select(.key != "manifest") | "- **\(.key)**: \(.value)"' >> release_notes.md + + # Add manifest information + manifest_url='${{ needs.distribute.outputs.manifest-url }}' + if [ -n "$manifest_url" ]; then + cat >> release_notes.md << EOF + + ### Auto-update manifest: + - **Manifest**: $manifest_url + EOF + fi + + # Add Homebrew information if tap is configured + if [ -n "${{ inputs.homebrew-tap }}" ]; then + cat >> release_notes.md << EOF + + ### Homebrew (coming soon): + \`\`\`bash + brew install ${{ inputs.homebrew-tap }}/$tool_name + \`\`\` + EOF + fi + + echo "โœ… Release notes generated" + + - name: Create GitHub release + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + tag_name: ${{ needs.prepare.outputs.release-tag }} + release_name: ${{ needs.prepare.outputs.release-name }} + body_path: release_notes.md + draft: false + prerelease: ${{ contains(needs.prepare.outputs.version, '-') }} + + - name: Upload release assets + run: | + echo "๐Ÿ“Ž Uploading release assets..." + + cd dist + + for file in *.tar.gz *.zip *.asc; do + if [ -f "$file" ]; then + echo "Uploading: $file" + + # Determine content type + case "$file" in + *.tar.gz) content_type="application/gzip" ;; + *.zip) content_type="application/zip" ;; + *.asc) content_type="application/pgp-signature" ;; + *) content_type="application/octet-stream" ;; + esac + + # Upload using GitHub CLI or curl + if command -v gh >/dev/null 2>&1; then + gh release upload "${{ needs.prepare.outputs.release-tag }}" "$file" + else + # Fallback to manual upload would go here + echo "โš ๏ธ GitHub CLI not available for asset upload" + fi + fi + done + + echo "โœ… Release assets uploaded" + + # Update Homebrew formula if tap is configured + homebrew: + name: ๐Ÿบ Update Homebrew Formula + runs-on: ubuntu-latest + needs: [prepare, distribute, release] + if: inputs.homebrew-tap != '' && secrets.homebrew-github-token != '' && needs.release.result == 'success' + outputs: + status: ${{ steps.update-formula.outputs.status }} + steps: + - name: Checkout Homebrew tap + uses: actions/checkout@v4 + with: + repository: ${{ inputs.homebrew-tap }} + token: ${{ secrets.homebrew-github-token }} + path: homebrew-tap + + - name: Update Homebrew formula + id: update-formula + run: | + echo "๐Ÿบ Updating Homebrew formula..." + + tool_name="${{ inputs.tool-name }}" + version="${{ needs.prepare.outputs.version }}" + urls='${{ needs.distribute.outputs.urls }}' + + cd homebrew-tap + + # Create or update formula file + formula_file="Formula/$tool_name.rb" + mkdir -p Formula + + # Get download URL for macOS (prefer darwin-x64) + download_url=$(echo "$urls" | jq -r '.["darwin-x64"] // .["darwin-arm64"] // empty') + + if [ -n "$download_url" ]; then + # Download and calculate SHA256 + curl -L "$download_url" -o temp_download + sha256=$(sha256sum temp_download | cut -d' ' -f1) + rm temp_download + + # Generate formula + class_name=$(echo "$tool_name" | sed 's/-//g' | awk '{print toupper(substr($0,1,1))substr($0,2)}') + + # Create Homebrew formula file + { + echo "class $class_name < Formula" + echo " desc \"CLI tool: $tool_name\"" + echo " homepage \"https://github.com/${{ github.repository }}\"" + echo " url \"$download_url\"" + echo " sha256 \"$sha256\"" + echo " version \"$version\"" + echo "" + echo " def install" + echo " bin.install \"$tool_name\"" + echo " end" + echo "" + echo " test do" + echo " system \"#{bin}/$tool_name\", \"--version\"" + echo " end" + echo "end" + } > "$formula_file" + + # Commit and push changes + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add "$formula_file" + + if git diff --staged --quiet; then + echo "status=no-changes" >> $GITHUB_OUTPUT + echo "โ„น๏ธ No changes to formula" + else + git commit -m "Update $tool_name to version $version" + git push + echo "status=updated" >> $GITHUB_OUTPUT + echo "โœ… Homebrew formula updated" + fi + else + echo "status=no-macos-binary" >> $GITHUB_OUTPUT + echo "โš ๏ธ No macOS binary found for Homebrew" + fi + + # Deployment summary + summary: + name: ๐Ÿ“‹ Release Summary + runs-on: ubuntu-latest + needs: [prepare, build, distribute, release, homebrew] + if: always() && needs.build.result == 'success' + steps: + - name: Display release summary + run: | + echo "๐Ÿ“‹ CLI Tools Release Summary" + echo "============================" + echo "Tool: ${{ inputs.tool-name }}" + echo "Version: ${{ needs.prepare.outputs.version }}" + echo "Platforms: ${{ needs.prepare.outputs.platforms }}" + echo "" + + if [ "${{ needs.distribute.result }}" = "success" ]; then + echo "๐Ÿ“ฆ Distribution: โœ… COMPLETED" + echo "S3 Bucket: ${{ inputs.s3-bucket }}" + echo "Manifest URL: ${{ needs.distribute.outputs.manifest-url }}" + else + echo "๐Ÿ“ฆ Distribution: โŒ FAILED" + fi + + if [ "${{ inputs.create-release }}" = "true" ]; then + if [ "${{ needs.release.result }}" = "success" ]; then + echo "๐Ÿš€ GitHub Release: โœ… CREATED" + echo "Release URL: ${{ needs.release.outputs.url }}" + else + echo "๐Ÿš€ GitHub Release: โŒ FAILED" + fi + else + echo "๐Ÿš€ GitHub Release: โญ๏ธ SKIPPED" + fi + + if [ "${{ needs.prepare.outputs.should-sign }}" = "true" ]; then + if [ "${{ needs.sign.result }}" = "success" ]; then + echo "๐Ÿ” GPG Signing: โœ… COMPLETED" + else + echo "๐Ÿ” GPG Signing: โŒ FAILED" + fi + else + echo "๐Ÿ” GPG Signing: โญ๏ธ DISABLED" + fi + + if [ -n "${{ inputs.homebrew-tap }}" ]; then + case "${{ needs.homebrew.outputs.status }}" in + updated) + echo "๐Ÿบ Homebrew: โœ… FORMULA UPDATED" + ;; + no-changes) + echo "๐Ÿบ Homebrew: โ„น๏ธ NO CHANGES NEEDED" + ;; + no-macos-binary) + echo "๐Ÿบ Homebrew: โš ๏ธ NO MACOS BINARY" + ;; + *) + echo "๐Ÿบ Homebrew: โŒ FAILED" + ;; + esac + else + echo "๐Ÿบ Homebrew: โญ๏ธ NOT CONFIGURED" + fi + + echo "" + echo "๐ŸŽ‰ CLI tools release pipeline completed!" \ No newline at end of file diff --git a/.github/workflows/docker-ecr-deploy.yml b/.github/workflows/docker-ecr-deploy.yml new file mode 100644 index 0000000..8cbee35 --- /dev/null +++ b/.github/workflows/docker-ecr-deploy.yml @@ -0,0 +1,629 @@ +name: ๐Ÿณ Docker ECR Deployment + +on: + workflow_call: + inputs: + # Core Configuration + aws-region: + description: "AWS region for ECR registry" + type: string + required: false + default: "ap-southeast-2" + ecr-repository: + description: "ECR repository name (required)" + type: string + required: true + dockerfile-path: + description: "Path to Dockerfile" + type: string + required: false + default: "Dockerfile" + build-context: + description: "Docker build context path" + type: string + required: false + default: "." + + # Platform and Build Configuration + platforms: + description: "Target platforms for multi-platform builds" + type: string + required: false + default: "linux/amd64,linux/arm64" + push-to-registry: + description: "Push built images to ECR registry" + type: boolean + required: false + default: true + + # Security and Scanning + vulnerability-scan: + description: "Enable container vulnerability scanning" + type: boolean + required: false + default: true + security-threshold: + description: "Security vulnerability threshold (CRITICAL/HIGH/MEDIUM/LOW)" + type: string + required: false + default: "HIGH" + + # Tagging Strategy + tag-strategy: + description: "Image tagging strategy (latest/semantic/branch/custom)" + type: string + required: false + default: "latest" + custom-tags: + description: "Custom tags (comma-separated) when using custom strategy" + type: string + required: false + default: "" + + # Build Optimization + cache-from: + description: "Cache sources for build optimization (comma-separated)" + type: string + required: false + default: "" + build-args: + description: "Docker build arguments as JSON object" + type: string + required: false + default: "{}" + target-stage: + description: "Target build stage for multi-stage Dockerfiles" + type: string + required: false + default: "" + + # Registry Management + cleanup-old-images: + description: "Clean up old images from ECR registry" + type: boolean + required: false + default: false + retention-count: + description: "Number of images to retain when cleaning up" + type: string + required: false + default: "10" + + # Container Signing + enable-signing: + description: "Enable container image signing with cosign" + type: boolean + required: false + default: false + + # Advanced Configuration + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + + secrets: + aws-access-key-id: + description: "AWS access key ID" + required: true + aws-secret-access-key: + description: "AWS secret access key" + required: true + container-signing-key: + description: "Private key for container signing (optional)" + required: false + + outputs: + image-uri: + description: "Full URI of the built container image" + value: ${{ jobs.build.outputs.image-uri }} + image-digest: + description: "SHA256 digest of the built image" + value: ${{ jobs.build.outputs.image-digest }} + image-tags: + description: "Applied image tags as JSON array" + value: ${{ jobs.build.outputs.image-tags }} + vulnerability-report: + description: "Container vulnerability scan results" + value: ${{ jobs.scan.outputs.vulnerability-report }} + +jobs: + # Validate inputs and prepare build configuration + prepare: + name: ๐Ÿ” Prepare Docker Build + runs-on: ubuntu-latest + outputs: + image-tags: ${{ steps.tag-config.outputs.tags }} + build-args: ${{ steps.build-config.outputs.args }} + cache-config: ${{ steps.cache-config.outputs.setup }} + platforms: ${{ steps.platform-config.outputs.platforms }} + security-scan: ${{ steps.security-config.outputs.enabled }} + steps: + - name: Validate required inputs + run: | + echo "๐Ÿ” Validating Docker build configuration..." + + if [ -z "${{ inputs.ecr-repository }}" ]; then + echo "โŒ Error: ecr-repository is required" + exit 1 + fi + + # Validate security threshold + case "${{ inputs.security-threshold }}" in + CRITICAL|HIGH|MEDIUM|LOW) + echo "โœ… Security threshold: ${{ inputs.security-threshold }}" + ;; + *) + echo "โŒ Error: security-threshold must be one of: CRITICAL, HIGH, MEDIUM, LOW" + exit 1 + ;; + esac + + # Validate tag strategy + case "${{ inputs.tag-strategy }}" in + latest|semantic|branch|custom) + echo "โœ… Tag strategy: ${{ inputs.tag-strategy }}" + ;; + *) + echo "โŒ Error: tag-strategy must be one of: latest, semantic, branch, custom" + exit 1 + ;; + esac + + # Validate build args JSON if provided + if [ "${{ inputs.build-args }}" != "{}" ]; then + echo '${{ inputs.build-args }}' | jq . > /dev/null + if [ $? -ne 0 ]; then + echo "โŒ Error: build-args must be valid JSON" + exit 1 + fi + fi + + echo "โœ… All inputs validated successfully" + + - name: Configure image tags + id: tag-config + run: | + echo "๐Ÿท๏ธ Configuring image tags..." + + tags="" + case "${{ inputs.tag-strategy }}" in + latest) + tags="latest" + if [ "${{ github.ref_type }}" = "tag" ]; then + tags="$tags,${{ github.ref_name }}" + fi + ;; + semantic) + if [ "${{ github.ref_type }}" = "tag" ]; then + tag_name="${{ github.ref_name }}" + tags="$tag_name" + # Extract semantic version components + if [[ $tag_name =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + tags="$tags,$major,$major.$minor,$major.$minor.$patch" + fi + else + tags="latest" + fi + ;; + branch) + branch_name=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]/-/g') + tags="$branch_name" + if [ "${{ github.ref_name }}" = "main" ] || [ "${{ github.ref_name }}" = "master" ]; then + tags="$tags,latest" + fi + ;; + custom) + if [ -n "${{ inputs.custom-tags }}" ]; then + tags="${{ inputs.custom-tags }}" + else + echo "โŒ Error: custom-tags must be provided when using custom strategy" + exit 1 + fi + ;; + esac + + # Add commit SHA tag + short_sha=$(echo "${{ github.sha }}" | cut -c1-7) + tags="$tags,sha-$short_sha" + + echo "tags=$tags" >> $GITHUB_OUTPUT + echo "โœ… Tags configured: $tags" + + - name: Configure build arguments + id: build-config + run: | + echo "โš™๏ธ Configuring build arguments..." + + build_args="" + + # Add default build args + build_args="$build_args --build-arg BUILDKIT_INLINE_CACHE=1" + build_args="$build_args --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + build_args="$build_args --build-arg VCS_REF=${{ github.sha }}" + build_args="$build_args --build-arg VERSION=${{ github.ref_name }}" + + # Add custom build args + if [ "${{ inputs.build-args }}" != "{}" ]; then + echo '${{ inputs.build-args }}' | jq -r 'to_entries[] | "--build-arg \(.key)=\(.value)"' | while read -r arg; do + build_args="$build_args $arg" + done + fi + + echo "args=$build_args" >> $GITHUB_OUTPUT + echo "โœ… Build arguments configured" + + - name: Configure cache settings + id: cache-config + run: | + echo "๐Ÿ’พ Configuring build cache..." + + cache_from="" + if [ -n "${{ inputs.cache-from }}" ]; then + # Convert comma-separated cache sources to buildx format + IFS=',' read -ra CACHE_SOURCES <<< "${{ inputs.cache-from }}" + for source in "${CACHE_SOURCES[@]}"; do + cache_from="$cache_from --cache-from type=registry,ref=$source" + done + fi + + # Add ECR cache source + ecr_uri="${{ inputs.ecr-repository }}:buildcache" + cache_from="$cache_from --cache-from type=registry,ref=$ecr_uri" + cache_to="--cache-to type=registry,ref=$ecr_uri,mode=max" + + echo "setup=$cache_from $cache_to" >> $GITHUB_OUTPUT + echo "โœ… Cache configuration prepared" + + - name: Configure platforms + id: platform-config + run: | + echo "๐Ÿ—๏ธ Configuring build platforms..." + + platforms="${{ inputs.platforms }}" + + # Validate platforms + IFS=',' read -ra PLATFORM_LIST <<< "$platforms" + for platform in "${PLATFORM_LIST[@]}"; do + case "$platform" in + linux/amd64|linux/arm64|linux/arm/v7|linux/arm/v8) + echo "โœ… Platform supported: $platform" + ;; + *) + echo "โš ๏ธ Warning: Unusual platform: $platform" + ;; + esac + done + + echo "platforms=$platforms" >> $GITHUB_OUTPUT + echo "โœ… Platforms configured: $platforms" + + - name: Configure security scanning + id: security-config + run: | + echo "๐Ÿ”’ Configuring security settings..." + + scan_enabled="${{ inputs.vulnerability-scan }}" + + if [ "$scan_enabled" = "true" ]; then + echo "enabled=true" >> $GITHUB_OUTPUT + echo "โœ… Vulnerability scanning enabled (threshold: ${{ inputs.security-threshold }})" + else + echo "enabled=false" >> $GITHUB_OUTPUT + echo "โ„น๏ธ Vulnerability scanning disabled" + fi + + # Build and push Docker image + build: + name: ๐Ÿ—๏ธ Build & Push Docker Image + runs-on: ubuntu-latest + needs: prepare + outputs: + image-uri: ${{ steps.build.outputs.image-uri }} + image-digest: ${{ steps.build.outputs.digest }} + image-tags: ${{ steps.build.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: ${{ needs.prepare.outputs.platforms }} + driver-opts: | + network=host + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + mask-password: 'true' + + - name: Create ECR repository if needed + run: | + echo "๐Ÿ—„๏ธ Ensuring ECR repository exists..." + + aws ecr describe-repositories --repository-names ${{ inputs.ecr-repository }} --region ${{ inputs.aws-region }} 2>/dev/null || { + echo "๐Ÿ“ฆ Creating ECR repository: ${{ inputs.ecr-repository }}" + aws ecr create-repository \ + --repository-name ${{ inputs.ecr-repository }} \ + --region ${{ inputs.aws-region }} \ + --image-scanning-configuration scanOnPush=true \ + --encryption-configuration encryptionType=AES256 + + # Set lifecycle policy + aws ecr put-lifecycle-policy \ + --repository-name ${{ inputs.ecr-repository }} \ + --region ${{ inputs.aws-region }} \ + --lifecycle-policy-text '{ + "rules": [ + { + "rulePriority": 1, + "description": "Keep last 30 images", + "selection": { + "tagStatus": "any", + "countType": "imageCountMoreThan", + "countNumber": 30 + }, + "action": { + "type": "expire" + } + } + ] + }' + } + + echo "โœ… ECR repository ready" + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.build-context }} + file: ${{ inputs.dockerfile-path }} + platforms: ${{ needs.prepare.outputs.platforms }} + push: ${{ inputs.push-to-registry }} + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ inputs.ecr-repository }}:${{ needs.prepare.outputs.image-tags }} + target: ${{ inputs.target-stage }} + cache-from: ${{ needs.prepare.outputs.cache-config }} + cache-to: type=inline + provenance: true + sbom: true + outputs: type=image,name=${{ steps.login-ecr.outputs.registry }}/${{ inputs.ecr-repository }},push=${{ inputs.push-to-registry }} + + - name: Extract build metadata + id: metadata + run: | + echo "๐Ÿ“‹ Extracting build metadata..." + + registry="${{ steps.login-ecr.outputs.registry }}" + repository="${{ inputs.ecr-repository }}" + + # Get image digest from build output + digest="${{ steps.build.outputs.digest }}" + + # Construct image URI + image_uri="$registry/$repository@$digest" + + # Format tags as JSON array + tags_array=$(echo "${{ needs.prepare.outputs.image-tags }}" | sed 's/,/","/g' | sed 's/^/["/' | sed 's/$/"]/') + + echo "image-uri=$image_uri" >> $GITHUB_OUTPUT + echo "digest=$digest" >> $GITHUB_OUTPUT + echo "tags=$tags_array" >> $GITHUB_OUTPUT + + echo "โœ… Image built: $image_uri" + echo "๐Ÿท๏ธ Tags applied: ${{ needs.prepare.outputs.image-tags }}" + + - name: Sign container image + if: inputs.enable-signing == true && secrets.container-signing-key != '' + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + + - name: Sign the container image + if: inputs.enable-signing == true && secrets.container-signing-key != '' + run: | + echo "โœ๏ธ Signing container image..." + + echo "${{ secrets.container-signing-key }}" > cosign.key + + cosign sign --key cosign.key \ + ${{ steps.metadata.outputs.image-uri }} + + rm cosign.key + echo "โœ… Image signed successfully" + + # Vulnerability scanning + scan: + name: ๐Ÿ” Security Vulnerability Scan + runs-on: ubuntu-latest + needs: [prepare, build] + if: needs.prepare.outputs.security-scan == 'true' && needs.build.result == 'success' + outputs: + vulnerability-report: ${{ steps.scan.outputs.report }} + security-status: ${{ steps.scan.outputs.status }} + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Run Trivy vulnerability scanner + id: scan + uses: aquasecurity/trivy-action@77137e9dc3ab1b329b7c8a38c2eb7475850a14e8 # master + with: + image-ref: ${{ needs.build.outputs.image-uri }} + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH,MEDIUM,LOW' + + - name: Analyze vulnerability results + id: analyze + run: | + echo "๐Ÿ” Analyzing vulnerability scan results..." + + # Parse SARIF results + critical_count=$(jq '[.runs[].results[] | select(.level == "error")] | length' trivy-results.sarif) + high_count=$(jq '[.runs[].results[] | select(.level == "warning")] | length' trivy-results.sarif) + medium_count=$(jq '[.runs[].results[] | select(.level == "note")] | length' trivy-results.sarif) + + echo "๐Ÿ”ด Critical vulnerabilities: $critical_count" + echo "๐ŸŸ  High vulnerabilities: $high_count" + echo "๐ŸŸก Medium vulnerabilities: $medium_count" + + # Check against threshold + case "${{ inputs.security-threshold }}" in + CRITICAL) + if [ "$critical_count" -gt 0 ]; then + echo "โŒ Critical vulnerabilities found - blocking deployment" + echo "status=failed" >> $GITHUB_OUTPUT + exit 1 + fi + ;; + HIGH) + if [ "$critical_count" -gt 0 ] || [ "$high_count" -gt 0 ]; then + echo "โŒ High or critical vulnerabilities found - blocking deployment" + echo "status=failed" >> $GITHUB_OUTPUT + exit 1 + fi + ;; + MEDIUM) + if [ "$critical_count" -gt 0 ] || [ "$high_count" -gt 0 ] || [ "$medium_count" -gt 0 ]; then + echo "โŒ Medium, high, or critical vulnerabilities found - blocking deployment" + echo "status=failed" >> $GITHUB_OUTPUT + exit 1 + fi + ;; + LOW) + echo "โ„น๏ธ All vulnerabilities reported for awareness" + ;; + esac + + echo "status=passed" >> $GITHUB_OUTPUT + echo "โœ… Vulnerability scan passed security threshold" + + # Create summary report + report=$(jq -c '{ + critical: [.runs[].results[] | select(.level == "error")] | length, + high: [.runs[].results[] | select(.level == "warning")] | length, + medium: [.runs[].results[] | select(.level == "note")] | length, + threshold: "${{ inputs.security-threshold }}", + status: "passed" + }' trivy-results.sarif) + + echo "report=$report" >> $GITHUB_OUTPUT + + - name: Upload Trivy scan results + uses: actions/upload-artifact@v4 + with: + name: trivy-results + path: trivy-results.sarif + retention-days: 30 + + - name: Upload to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-results.sarif + + # Registry cleanup + cleanup: + name: ๐Ÿงน Registry Cleanup + runs-on: ubuntu-latest + needs: [prepare, build, scan] + if: inputs.cleanup-old-images == true && (needs.scan.result == 'success' || needs.scan.result == 'skipped') && needs.build.result == 'success' + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Clean up old images + run: | + echo "๐Ÿงน Cleaning up old images from ECR..." + + # Get list of images sorted by pushed date + images=$(aws ecr describe-images \ + --repository-name ${{ inputs.ecr-repository }} \ + --region ${{ inputs.aws-region }} \ + --query 'sort_by(imageDetails, &imagePushedAt)[:-${{ inputs.retention-count }}].imageDigest' \ + --output text) + + if [ -n "$images" ] && [ "$images" != "None" ]; then + echo "๐Ÿ—‘๏ธ Found $(echo $images | wc -w) images to delete" + + for digest in $images; do + echo "Deleting image: $digest" + aws ecr batch-delete-image \ + --repository-name ${{ inputs.ecr-repository }} \ + --region ${{ inputs.aws-region }} \ + --image-ids imageDigest=$digest + done + + echo "โœ… Cleanup completed" + else + echo "โ„น๏ธ No images to clean up" + fi + + # Deployment summary + summary: + name: ๐Ÿ“‹ Deployment Summary + runs-on: ubuntu-latest + needs: [prepare, build, scan, cleanup] + if: always() && needs.build.result == 'success' + steps: + - name: Display deployment summary + run: | + echo "๐Ÿ“‹ Docker ECR Deployment Summary" + echo "==================================" + echo "Repository: ${{ inputs.ecr-repository }}" + echo "Region: ${{ inputs.aws-region }}" + echo "Platforms: ${{ needs.prepare.outputs.platforms }}" + echo "Image URI: ${{ needs.build.outputs.image-uri }}" + echo "Tags: ${{ needs.prepare.outputs.image-tags }}" + echo "" + + if [ "${{ inputs.vulnerability-scan }}" = "true" ]; then + if [ "${{ needs.scan.result }}" = "success" ]; then + echo "๐Ÿ”’ Security scan: โœ… PASSED" + echo "Security threshold: ${{ inputs.security-threshold }}" + else + echo "๐Ÿ”’ Security scan: โŒ FAILED" + fi + else + echo "๐Ÿ”’ Security scan: โญ๏ธ SKIPPED" + fi + + if [ "${{ inputs.cleanup-old-images }}" = "true" ]; then + if [ "${{ needs.cleanup.result }}" = "success" ]; then + echo "๐Ÿงน Registry cleanup: โœ… COMPLETED" + else + echo "๐Ÿงน Registry cleanup: โŒ FAILED" + fi + else + echo "๐Ÿงน Registry cleanup: โญ๏ธ SKIPPED" + fi + + if [ "${{ inputs.enable-signing }}" = "true" ]; then + echo "โœ๏ธ Image signing: โœ… ENABLED" + else + echo "โœ๏ธ Image signing: โญ๏ธ DISABLED" + fi + + echo "" + echo "๐ŸŽ‰ Docker deployment completed successfully!" \ No newline at end of file diff --git a/.github/workflows/magento-cloud-deploy.yml b/.github/workflows/magento-cloud-deploy.yml new file mode 100644 index 0000000..3555323 --- /dev/null +++ b/.github/workflows/magento-cloud-deploy.yml @@ -0,0 +1,567 @@ +name: ๐Ÿ›๏ธ Magento Cloud Deployment + +on: + workflow_call: + inputs: + # Magento Cloud Configuration + magento-cloud-project-id: + description: "Magento Cloud project ID (required)" + type: string + required: true + environment: + description: "Target environment (integration/staging/production)" + type: string + required: false + default: "integration" + + # PHP Configuration + php-version: + description: "PHP version for Magento (8.1, 8.2, 8.3)" + type: string + required: false + default: "8.1" + memory-limit: + description: "PHP memory limit for compilation (-1 for unlimited)" + type: string + required: false + default: "-1" + + # Magento-specific Configuration + apply-patches: + description: "Apply ECE patches before deployment" + type: boolean + required: false + default: true + di-compile: + description: "Run dependency injection compilation" + type: boolean + required: false + default: true + + # Deployment Control + manual-deploy: + description: "Require manual approval for production deployments" + type: boolean + required: false + default: false + + # Monitoring and Reporting + newrelic-app-id: + description: "NewRelic application ID for deployment markers (optional)" + type: string + required: false + default: "" + + # Advanced Configuration + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + + secrets: + magento-cloud-cli-token: + description: "Magento Cloud CLI token for authentication" + required: true + newrelic-api-key: + description: "NewRelic API key for deployment markers (optional)" + required: false + cst-reporting-token: + description: "CST system reporting token (optional)" + required: false + + outputs: + deployment-url: + description: "URL of the deployed Magento application" + value: ${{ jobs.deploy.outputs.deployment-url }} + deployment-id: + description: "Magento Cloud deployment ID" + value: ${{ jobs.deploy.outputs.deployment-id }} + +jobs: + # Validate inputs and prepare deployment configuration + prepare: + name: ๐Ÿ” Prepare Magento Deployment + runs-on: ubuntu-latest + outputs: + php-container: ${{ steps.php-config.outputs.container }} + memory-limit: ${{ steps.php-config.outputs.memory-limit }} + deployment-strategy: ${{ steps.deployment-config.outputs.strategy }} + requires-approval: ${{ steps.deployment-config.outputs.requires-approval }} + steps: + - name: Validate required inputs + run: | + if [ -z "${{ inputs.magento-cloud-project-id }}" ]; then + echo "โŒ Error: magento-cloud-project-id is required" + exit 1 + fi + + if [ "${{ inputs.environment }}" != "integration" ] && [ "${{ inputs.environment }}" != "staging" ] && [ "${{ inputs.environment }}" != "production" ]; then + echo "โŒ Error: environment must be one of: integration, staging, production" + exit 1 + fi + + case "${{ inputs.php-version }}" in + "8.1"|"8.2"|"8.3") + echo "โœ… PHP version ${{ inputs.php-version }} is supported" + ;; + *) + echo "โŒ Error: php-version must be one of: 8.1, 8.2, 8.3" + exit 1 + ;; + esac + + echo "โœ… All required inputs validated" + + - name: Configure PHP environment + id: php-config + run: | + # Set Magento-optimized PHP container based on version + case "${{ inputs.php-version }}" in + "8.1") + echo "container=magento/magento-cloud-docker-php:8.1-cli" >> $GITHUB_OUTPUT + ;; + "8.2") + echo "container=magento/magento-cloud-docker-php:8.2-cli" >> $GITHUB_OUTPUT + ;; + "8.3") + echo "container=magento/magento-cloud-docker-php:8.3-cli" >> $GITHUB_OUTPUT + ;; + esac + + # Configure memory limit for DI compilation + MEMORY_LIMIT="${{ inputs.memory-limit }}" + if [ "$MEMORY_LIMIT" = "-1" ]; then + MEMORY_LIMIT="unlimited" + fi + echo "memory-limit=${MEMORY_LIMIT}" >> $GITHUB_OUTPUT + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” PHP configuration:" + echo " Version: ${{ inputs.php-version }}" + echo " Container: $(cat $GITHUB_OUTPUT | grep container | cut -d'=' -f2-)" + echo " Memory Limit: $(cat $GITHUB_OUTPUT | grep memory-limit | cut -d'=' -f2-)" + fi + + - name: Configure deployment strategy + id: deployment-config + run: | + # Determine deployment strategy based on environment + case "${{ inputs.environment }}" in + "production") + echo "strategy=production" >> $GITHUB_OUTPUT + if [ "${{ inputs.manual-deploy }}" = "true" ]; then + echo "requires-approval=true" >> $GITHUB_OUTPUT + else + echo "requires-approval=false" >> $GITHUB_OUTPUT + fi + ;; + "staging") + echo "strategy=staging" >> $GITHUB_OUTPUT + echo "requires-approval=false" >> $GITHUB_OUTPUT + ;; + "integration") + echo "strategy=integration" >> $GITHUB_OUTPUT + echo "requires-approval=false" >> $GITHUB_OUTPUT + ;; + esac + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” Deployment configuration:" + echo " Environment: ${{ inputs.environment }}" + echo " Strategy: $(cat $GITHUB_OUTPUT | grep strategy | cut -d'=' -f2-)" + echo " Requires Approval: $(cat $GITHUB_OUTPUT | grep requires-approval | cut -d'=' -f2-)" + fi + + # Pre-deployment preparation and validation + pre-deploy: + name: ๐Ÿ› ๏ธ Pre-deployment Setup + runs-on: ubuntu-latest + needs: [prepare] + container: + image: ${{ needs.prepare.outputs.php-container }} + options: --user root + steps: + - name: Checkout code with full git history + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full git history required for Magento Cloud + + - name: Install Magento Cloud CLI + run: | + echo "๐Ÿ“ฆ Installing Magento Cloud CLI..." + curl -fsS https://accounts.magento.cloud/cli/installer | php + export PATH=$HOME/.magento-cloud/bin:$PATH + echo "$HOME/.magento-cloud/bin" >> $GITHUB_PATH + + # Verify installation + magento-cloud --version + echo "โœ… Magento Cloud CLI installed successfully" + + - name: Configure Magento Cloud CLI authentication + run: | + echo "๐Ÿ” Configuring Magento Cloud authentication..." + magento-cloud auth:login --token "${{ secrets.magento-cloud-cli-token }}" + echo "โœ… Authentication configured" + + - name: Validate project access + run: | + echo "๐Ÿ” Validating project access..." + magento-cloud project:info --project "${{ inputs.magento-cloud-project-id }}" --format plain + echo "โœ… Project access validated" + + - name: Configure PHP for Magento + run: | + echo "๐Ÿ”ง Configuring PHP for Magento..." + + # Set memory limit for DI compilation + if [ "${{ needs.prepare.outputs.memory-limit }}" = "unlimited" ]; then + echo "memory_limit = -1" > /usr/local/etc/php/conf.d/memory-limit.ini + else + echo "memory_limit = ${{ needs.prepare.outputs.memory-limit }}" > /usr/local/etc/php/conf.d/memory-limit.ini + fi + + # Magento-specific PHP settings + cat > /usr/local/etc/php/conf.d/magento.ini << 'EOF' + max_execution_time = 18000 + max_input_vars = 10000 + upload_max_filesize = 64M + post_max_size = 64M + realpath_cache_size = 10M + realpath_cache_ttl = 7200 + opcache.memory_consumption = 512 + opcache.max_accelerated_files = 60000 + opcache.consistency_checks = 0 + opcache.validate_timestamps = 0 + opcache.enable_cli = 1 + EOF + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” PHP configuration:" + php -i | grep memory_limit + php -i | grep max_execution_time + fi + + - name: Install Composer dependencies + run: | + echo "๐Ÿ“ฆ Installing Composer dependencies..." + + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # Use Composer with Magento-specific optimizations + composer install \ + --no-dev \ + --optimize-autoloader \ + --no-interaction \ + --prefer-dist \ + $debug + + echo "โœ… Composer dependencies installed" + + - name: Apply ECE patches + if: inputs.apply-patches == true + run: | + echo "๐Ÿฉน Applying ECE patches..." + + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # Apply Magento Cloud patches + if [ -f "vendor/magento/ece-tools/bin/ece-patches" ]; then + php vendor/magento/ece-tools/bin/ece-patches apply $debug + echo "โœ… ECE patches applied successfully" + else + echo "โš ๏ธ ECE patches tool not found, skipping patch application" + fi + + - name: Run dependency injection compilation + if: inputs.di-compile == true + run: | + echo "โš™๏ธ Running dependency injection compilation..." + + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # DI compilation with unlimited memory + php -dmemory_limit=-1 bin/magento setup:di:compile $debug + echo "โœ… DI compilation completed successfully" + + - name: Generate static content (if required) + run: | + echo "๐ŸŽจ Checking for static content deployment..." + + # Only run if in production mode or if static content is missing + if php bin/magento deploy:mode:show | grep -q "production" || [ ! -d "pub/static/_cache" ]; then + echo "Generating static content..." + php -dmemory_limit=-1 bin/magento setup:static-content:deploy -f + echo "โœ… Static content generated" + else + echo "โ„น๏ธ Static content generation skipped (developer mode or already exists)" + fi + + # Manual approval gate for production deployments + approval: + name: ๐Ÿšฆ Production Deployment Approval + runs-on: ubuntu-latest + needs: [prepare, pre-deploy] + if: needs.prepare.outputs.requires-approval == 'true' + environment: production-approval + steps: + - name: Request manual approval + run: | + echo "๐Ÿšฆ Manual approval required for production deployment" + echo "Project: ${{ inputs.magento-cloud-project-id }}" + echo "Environment: ${{ inputs.environment }}" + echo "โš ๏ธ Please review and approve this deployment to continue" + + # Deploy to Magento Cloud + deploy: + name: ๐Ÿš€ Deploy to Magento Cloud + runs-on: ubuntu-latest + needs: [prepare, pre-deploy, approval] + if: always() && needs.pre-deploy.result == 'success' && (needs.approval.result == 'success' || needs.prepare.outputs.requires-approval == 'false') + environment: ${{ inputs.environment }} + container: + image: ${{ needs.prepare.outputs.php-container }} + options: --user root + outputs: + deployment-url: ${{ steps.deploy-info.outputs.url }} + deployment-id: ${{ steps.deploy-info.outputs.id }} + steps: + - name: Checkout code with full git history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Magento Cloud CLI + run: | + curl -fsS https://accounts.magento.cloud/cli/installer | php + export PATH=$HOME/.magento-cloud/bin:$PATH + echo "$HOME/.magento-cloud/bin" >> $GITHUB_PATH + magento-cloud auth:login --token "${{ secrets.magento-cloud-cli-token }}" + + - name: Create NewRelic deployment marker (start) + if: inputs.newrelic-app-id != '' && secrets.newrelic-api-key != '' + run: | + echo "๐Ÿ“Š Creating NewRelic deployment marker (start)..." + + curl -X POST "https://api.newrelic.com/v2/applications/${{ inputs.newrelic-app-id }}/deployments.json" \ + -H "X-Api-Key: ${{ secrets.newrelic-api-key }}" \ + -H "Content-Type: application/json" \ + -d '{ + "deployment": { + "revision": "${{ github.sha }}", + "changelog": "Magento Cloud deployment started", + "description": "Deployment to ${{ inputs.environment }} environment", + "user": "${{ github.actor }}" + } + }' + + echo "โœ… NewRelic deployment marker created" + + - name: Deploy to Magento Cloud + id: deployment + run: | + echo "๐Ÿš€ Starting deployment to ${{ inputs.environment }}..." + + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # Set project context + magento-cloud project:set-remote "${{ inputs.magento-cloud-project-id }}" + + # Deploy based on environment type + case "${{ inputs.environment }}" in + "integration") + # Push to integration environment + echo "Deploying to integration environment..." + magento-cloud push --force --wait $debug + ;; + "staging"|"production") + # Merge to staging/production branch + echo "Deploying to ${{ inputs.environment }} environment..." + magento-cloud merge --environment "${{ inputs.environment }}" --wait $debug + ;; + esac + + echo "โœ… Deployment completed successfully" + + - name: Get deployment information + id: deploy-info + run: | + echo "๐Ÿ“‹ Retrieving deployment information..." + + # Get environment URL + URL=$(magento-cloud url --environment "${{ inputs.environment }}" --project "${{ inputs.magento-cloud-project-id }}" --pipe) + echo "url=${URL}" >> $GITHUB_OUTPUT + + # Get deployment ID + DEPLOYMENT_ID=$(magento-cloud activity:list --environment "${{ inputs.environment }}" --type push --limit 1 --format csv --columns id --no-header | head -1) + echo "id=${DEPLOYMENT_ID}" >> $GITHUB_OUTPUT + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” Deployment information:" + echo " URL: ${URL}" + echo " Deployment ID: ${DEPLOYMENT_ID}" + fi + + - name: Verify deployment health + run: | + echo "๐Ÿฅ Verifying deployment health..." + + URL="${{ steps.deploy-info.outputs.url }}" + + # Wait for the site to be available + for i in {1..10}; do + if curl -f -s -o /dev/null "$URL"; then + echo "โœ… Site is responding successfully" + break + else + echo "โณ Waiting for site to respond (attempt $i/10)..." + sleep 30 + fi + + if [ $i -eq 10 ]; then + echo "โŒ Site health check failed after 10 attempts" + exit 1 + fi + done + + - name: Report version to CST systems + if: secrets.cst-reporting-token != '' + run: | + echo "๐Ÿ“ก Reporting deployment to CST systems..." + + # Extract version information + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "unknown") + COMMIT_SHA="${{ github.sha }}" + + # Report to CST endpoint + curl -X POST "$CST_ENDPOINT/deployments" \ + -H "Authorization: Bearer ${{ secrets.cst-reporting-token }}" \ + -H "Content-Type: application/json" \ + -d '{ + "project_id": "${{ inputs.magento-cloud-project-id }}", + "environment": "${{ inputs.environment }}", + "version": "'$VERSION'", + "commit_sha": "'$COMMIT_SHA'", + "deployment_url": "${{ steps.deploy-info.outputs.url }}", + "deployment_id": "${{ steps.deploy-info.outputs.id }}", + "deployed_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", + "deployed_by": "${{ github.actor }}" + }' + + echo "โœ… Version reported to CST systems" + env: + CST_ENDPOINT: "https://cst-api.example.com" # Configure as needed + + - name: Create NewRelic deployment marker (complete) + if: inputs.newrelic-app-id != '' && secrets.newrelic-api-key != '' + run: | + echo "๐Ÿ“Š Creating NewRelic deployment marker (complete)..." + + curl -X POST "https://api.newrelic.com/v2/applications/${{ inputs.newrelic-app-id }}/deployments.json" \ + -H "X-Api-Key: ${{ secrets.newrelic-api-key }}" \ + -H "Content-Type: application/json" \ + -d '{ + "deployment": { + "revision": "${{ github.sha }}", + "changelog": "Magento Cloud deployment completed successfully", + "description": "Deployment to ${{ inputs.environment }} completed at ${{ steps.deploy-info.outputs.url }}", + "user": "${{ github.actor }}" + } + }' + + echo "โœ… NewRelic deployment marker updated" + + - name: Generate deployment summary + run: | + echo "## ๐Ÿ›๏ธ Magento Cloud Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Project ID** | ${{ inputs.magento-cloud-project-id }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Environment** | ${{ inputs.environment }} |" >> $GITHUB_STEP_SUMMARY + echo "| **PHP Version** | ${{ inputs.php-version }} |" >> $GITHUB_STEP_SUMMARY + echo "| **ECE Patches** | ${{ inputs.apply-patches && 'โœ… Applied' || 'โŒ Skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **DI Compilation** | ${{ inputs.di-compile && 'โœ… Completed' || 'โŒ Skipped' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Deployment ID** | ${{ steps.deploy-info.outputs.id }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Site URL** | [${{ steps.deploy-info.outputs.url }}](${{ steps.deploy-info.outputs.url }}) |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ง Technical Details" >> $GITHUB_STEP_SUMMARY + echo "- **Memory Limit**: ${{ needs.prepare.outputs.memory-limit }}" >> $GITHUB_STEP_SUMMARY + echo "- **Container**: ${{ needs.prepare.outputs.php-container }}" >> $GITHUB_STEP_SUMMARY + echo "- **Git Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Deployed By**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ inputs.newrelic-app-id }}" != "" ]; then + echo "### ๐Ÿ“Š Monitoring" >> $GITHUB_STEP_SUMMARY + echo "- **NewRelic App ID**: ${{ inputs.newrelic-app-id }}" >> $GITHUB_STEP_SUMMARY + echo "- **Deployment Markers**: โœ… Created" >> $GITHUB_STEP_SUMMARY + fi + + case "${{ inputs.environment }}" in + "production") + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐ŸŒ Production Deployment" >> $GITHUB_STEP_SUMMARY + echo "Your Magento store is now live at:" >> $GITHUB_STEP_SUMMARY + echo "**[${{ steps.deploy-info.outputs.url }}](${{ steps.deploy-info.outputs.url }})**" >> $GITHUB_STEP_SUMMARY + ;; + "staging") + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿš€ Staging Environment" >> $GITHUB_STEP_SUMMARY + echo "Staging environment updated successfully:" >> $GITHUB_STEP_SUMMARY + echo "**[${{ steps.deploy-info.outputs.url }}](${{ steps.deploy-info.outputs.url }})**" >> $GITHUB_STEP_SUMMARY + ;; + *) + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ง Integration Environment" >> $GITHUB_STEP_SUMMARY + echo "Integration environment deployed for testing:" >> $GITHUB_STEP_SUMMARY + echo "**[${{ steps.deploy-info.outputs.url }}](${{ steps.deploy-info.outputs.url }})**" >> $GITHUB_STEP_SUMMARY + ;; + esac + + # Post-deployment monitoring and reporting + post-deploy: + name: ๐Ÿ” Post-deployment Monitoring + runs-on: ubuntu-latest + needs: [prepare, deploy] + if: always() && needs.deploy.result == 'success' + steps: + - name: Monitor deployment performance + run: | + echo "๐Ÿ“Š Monitoring deployment performance..." + + URL="${{ needs.deploy.outputs.deployment-url }}" + + # Basic performance check + RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}\n' "$URL") + echo "Response time: ${RESPONSE_TIME} seconds" + + if (( $(echo "$RESPONSE_TIME > 5.0" | bc -l) )); then + echo "โš ๏ธ Warning: Response time is slower than expected (${RESPONSE_TIME}s > 5.0s)" + else + echo "โœ… Response time is acceptable (${RESPONSE_TIME}s)" + fi + + - name: Generate performance report + run: | + echo "## ๐Ÿ“Š Performance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Post-deployment monitoring completed for environment: **${{ inputs.environment }}**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. โœ… Verify core functionality on the deployed site" >> $GITHUB_STEP_SUMMARY + echo "2. ๐Ÿงช Run smoke tests against the environment" >> $GITHUB_STEP_SUMMARY + echo "3. ๐Ÿ‘€ Monitor application logs for any issues" >> $GITHUB_STEP_SUMMARY + echo "4. ๐Ÿ“ˆ Check NewRelic dashboards for performance metrics" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/php-quality-checks.yml b/.github/workflows/php-quality-checks.yml new file mode 100644 index 0000000..c00e247 --- /dev/null +++ b/.github/workflows/php-quality-checks.yml @@ -0,0 +1,607 @@ +name: ๐Ÿ˜ PHP Quality Checks + +on: + workflow_call: + inputs: + # PHP Configuration + php-version: + description: "PHP version to use (8.1, 8.2, 8.3)" + type: string + required: false + default: "8.1" + + # PHPStan Configuration + phpstan-level: + description: "PHPStan analysis level (1-9)" + type: string + required: false + default: "6" + skip-phpstan: + description: "Skip PHPStan static analysis" + type: boolean + required: false + default: false + + # PHP CodeSniffer Configuration + coding-standard: + description: "Coding standard to use (Magento2, PSR12, PSR2)" + type: string + required: false + default: "Magento2" + skip-phpcs: + description: "Skip PHP CodeSniffer checks" + type: boolean + required: false + default: false + + # Testing Configuration + coverage-threshold: + description: "Code coverage threshold percentage (0-100)" + type: string + required: false + default: "80" + skip-tests: + description: "Skip PHP unit testing" + type: boolean + required: false + default: false + + # Composer Configuration + composer-args: + description: "Additional composer install arguments" + type: string + required: false + default: "" + + # System Configuration + memory-limit: + description: "PHP memory limit for analysis tools" + type: string + required: false + default: "512M" + + # Debug Configuration + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + +jobs: + # Validate configuration and detect available tools + prepare: + name: ๐Ÿ” Prepare PHP Analysis + runs-on: ubuntu-latest + outputs: + has-composer: ${{ steps.check-files.outputs.has-composer }} + has-phpunit: ${{ steps.check-tools.outputs.has-phpunit }} + has-phpstan-config: ${{ steps.check-tools.outputs.has-phpstan-config }} + has-phpcs-config: ${{ steps.check-tools.outputs.has-phpcs-config }} + has-phpmd-config: ${{ steps.check-tools.outputs.has-phpmd-config }} + php-container: ${{ steps.php-config.outputs.container }} + steps: + - uses: actions/checkout@v4 + + - name: Check for required files + id: check-files + run: | + if [ -f "composer.json" ]; then + echo "โœ… composer.json found" + echo "has-composer=true" >> $GITHUB_OUTPUT + else + echo "โŒ composer.json not found" + echo "has-composer=false" >> $GITHUB_OUTPUT + fi + + - name: Validate PHP version + run: | + case "${{ inputs.php-version }}" in + 8.1|8.2|8.3) + echo "โœ… PHP version ${{ inputs.php-version }} is supported" + ;; + *) + echo "โŒ Error: PHP version must be 8.1, 8.2, or 8.3" + exit 1 + ;; + esac + + - name: Validate PHPStan level + run: | + level="${{ inputs.phpstan-level }}" + if [[ "$level" =~ ^[1-9]$ ]]; then + echo "โœ… PHPStan level $level is valid" + else + echo "โŒ Error: PHPStan level must be between 1-9" + exit 1 + fi + + - name: Validate coding standard + run: | + case "${{ inputs.coding-standard }}" in + Magento2|PSR12|PSR2) + echo "โœ… Coding standard ${{ inputs.coding-standard }} is supported" + ;; + *) + echo "โŒ Error: Coding standard must be Magento2, PSR12, or PSR2" + exit 1 + ;; + esac + + - name: Set PHP container configuration + id: php-config + run: | + container="php:${{ inputs.php-version }}-cli-alpine" + echo "container=$container" >> $GITHUB_OUTPUT + echo "๐Ÿ˜ Using PHP container: $container" + + - name: Check for analysis tool configurations + id: check-tools + run: | + # Check for PHPUnit + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] || [ -f "phpunit.dist.xml" ]; then + echo "โœ… PHPUnit configuration found" + echo "has-phpunit=true" >> $GITHUB_OUTPUT + else + echo "โ„น๏ธ No PHPUnit configuration found" + echo "has-phpunit=false" >> $GITHUB_OUTPUT + fi + + # Check for PHPStan + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ] || [ -f "phpstan.dist.neon" ]; then + echo "โœ… PHPStan configuration found" + echo "has-phpstan-config=true" >> $GITHUB_OUTPUT + else + echo "โ„น๏ธ No PHPStan configuration found, will use defaults" + echo "has-phpstan-config=false" >> $GITHUB_OUTPUT + fi + + # Check for PHPCS + if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ] || [ -f "phpcs.dist.xml" ] || [ -f ".phpcs.xml" ]; then + echo "โœ… PHPCS configuration found" + echo "has-phpcs-config=true" >> $GITHUB_OUTPUT + else + echo "โ„น๏ธ No PHPCS configuration found, will use standard" + echo "has-phpcs-config=false" >> $GITHUB_OUTPUT + fi + + # Check for PHPMD + if [ -f "phpmd.xml" ] || [ -f "phpmd.xml.dist" ] || [ -f "phpmd.dist.xml" ]; then + echo "โœ… PHPMD configuration found" + echo "has-phpmd-config=true" >> $GITHUB_OUTPUT + else + echo "โ„น๏ธ No PHPMD configuration found, will use defaults" + echo "has-phpmd-config=false" >> $GITHUB_OUTPUT + fi + + # Install Composer dependencies with caching + install: + name: ๐Ÿ“ฆ Install Dependencies + needs: prepare + if: needs.prepare.outputs.has-composer == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Get Composer cache directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-php${{ inputs.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer-php${{ inputs.php-version }}- + ${{ runner.os }}-composer- + + - name: Install system dependencies + run: | + apk add --no-cache git unzip + + - name: Install Composer + run: | + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + composer --version + + - name: Validate composer.json and composer.lock + run: | + composer validate --strict ${{ inputs.debug && '--verbose' || '' }} + + - name: Install dependencies + run: | + php -ini | grep memory_limit + php -d memory_limit=${{ inputs.memory-limit }} /usr/local/bin/composer install \ + --no-progress \ + --no-suggest \ + --no-interaction \ + --optimize-autoloader \ + --classmap-authoritative \ + ${{ inputs.composer-args }} \ + ${{ inputs.debug && '--verbose' || '' }} + + # Archive vendor directory for other jobs + - name: Archive vendor directory + run: tar -czf vendor.tar.gz vendor/ + + - uses: actions/upload-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + path: vendor.tar.gz + retention-days: 1 + + # Composer security audit + security-audit: + name: ๐Ÿ”’ Security Audit + needs: [prepare, install] + if: needs.prepare.outputs.has-composer == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Install Composer + run: | + apk add --no-cache git unzip + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + - uses: actions/download-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + + - name: Extract vendor directory + run: tar -xzf vendor.tar.gz + + - name: Run Composer security audit + run: | + echo "๐Ÿ”’ Running Composer security audit..." + composer audit --format=table ${{ inputs.debug && '--verbose' || '' }} + + # PHPStan static analysis + phpstan: + name: ๐Ÿ” PHPStan Analysis + needs: [prepare, install] + if: | + inputs.skip-phpstan == false && + needs.prepare.outputs.has-composer == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: apk add --no-cache git + + - uses: actions/download-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + + - name: Extract vendor directory + run: tar -xzf vendor.tar.gz + + - name: Cache PHPStan results + uses: actions/cache@v4 + with: + path: /tmp/phpstan + key: phpstan-php${{ inputs.php-version }}-${{ inputs.phpstan-level }}-${{ hashFiles('**/*.php') }} + restore-keys: | + phpstan-php${{ inputs.php-version }}-${{ inputs.phpstan-level }}- + phpstan-php${{ inputs.php-version }}- + + - name: Run PHPStan analysis + run: | + echo "๐Ÿ” Running PHPStan analysis (Level ${{ inputs.phpstan-level }})..." + + # Check if PHPStan is installed + if [ -f "vendor/bin/phpstan" ]; then + phpstan_cmd="vendor/bin/phpstan" + else + echo "Installing PHPStan globally..." + composer global require phpstan/phpstan:^1.0 + phpstan_cmd="phpstan" + fi + + # Build PHPStan command + cmd="php -d memory_limit=${{ inputs.memory-limit }} $phpstan_cmd analyse" + + # Use existing config or set defaults + if [ "${{ needs.prepare.outputs.has-phpstan-config }}" = "true" ]; then + echo "โœ… Using existing PHPStan configuration" + else + echo "โ„น๏ธ Using default PHPStan configuration" + cmd="$cmd --level=${{ inputs.phpstan-level }} src/ app/" + fi + + # Add debug flags if enabled + if [ "${{ inputs.debug }}" = "true" ]; then + cmd="$cmd --verbose --debug" + fi + + # Execute PHPStan + eval $cmd + + # PHP CodeSniffer style checks + phpcs: + name: ๐ŸŽจ Code Style Check + needs: [prepare, install] + if: | + inputs.skip-phpcs == false && + needs.prepare.outputs.has-composer == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: apk add --no-cache git + + - uses: actions/download-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + + - name: Extract vendor directory + run: tar -xzf vendor.tar.gz + + - name: Run PHP CodeSniffer + run: | + echo "๐ŸŽจ Running PHP CodeSniffer (${{ inputs.coding-standard }} standard)..." + + # Check if PHPCS is installed + if [ -f "vendor/bin/phpcs" ]; then + phpcs_cmd="vendor/bin/phpcs" + else + echo "Installing PHP CodeSniffer globally..." + composer global require squizlabs/php_codesniffer:^3.0 + phpcs_cmd="phpcs" + + # Install Magento2 standard if needed + if [ "${{ inputs.coding-standard }}" = "Magento2" ]; then + composer global require magento/magento-coding-standard:* + fi + fi + + # Build PHPCS command + cmd="php -d memory_limit=${{ inputs.memory-limit }} $phpcs_cmd" + + # Use existing config or set standard + if [ "${{ needs.prepare.outputs.has-phpcs-config }}" = "true" ]; then + echo "โœ… Using existing PHPCS configuration" + else + echo "โ„น๏ธ Using ${{ inputs.coding-standard }} coding standard" + cmd="$cmd --standard=${{ inputs.coding-standard }} src/ app/" + fi + + # Add output formatting and debug flags + cmd="$cmd --report=full --colors" + if [ "${{ inputs.debug }}" = "true" ]; then + cmd="$cmd --verbose" + fi + + # Execute PHPCS + eval $cmd + + # PHPMD mess detection + phpmd: + name: ๐Ÿงน Mess Detection + needs: [prepare, install] + if: needs.prepare.outputs.has-composer == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: apk add --no-cache git + + - uses: actions/download-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + + - name: Extract vendor directory + run: tar -xzf vendor.tar.gz + + - name: Run PHP Mess Detector + run: | + echo "๐Ÿงน Running PHP Mess Detector..." + + # Check if PHPMD is installed + if [ -f "vendor/bin/phpmd" ]; then + phpmd_cmd="vendor/bin/phpmd" + else + echo "Installing PHPMD globally..." + composer global require phpmd/phpmd:^2.0 + phpmd_cmd="phpmd" + fi + + # Build PHPMD command + cmd="php -d memory_limit=${{ inputs.memory-limit }} $phpmd_cmd" + + # Use existing config or set defaults + if [ "${{ needs.prepare.outputs.has-phpmd-config }}" = "true" ]; then + echo "โœ… Using existing PHPMD configuration" + cmd="$cmd src/,app/ text phpmd.xml" + else + echo "โ„น๏ธ Using default PHPMD rules" + cmd="$cmd src/,app/ text cleancode,codesize,controversial,design,naming,unusedcode" + fi + + # Add debug flags if enabled + if [ "${{ inputs.debug }}" = "true" ]; then + cmd="$cmd --verbose" + fi + + # Execute PHPMD (allow to fail as it's often too strict) + eval $cmd || echo "โš ๏ธ PHPMD detected potential issues (non-blocking)" + + # PHPUnit testing with coverage + test: + name: ๐Ÿงช Unit Tests + needs: [prepare, install] + if: | + inputs.skip-tests == false && + needs.prepare.outputs.has-composer == 'true' && + needs.prepare.outputs.has-phpunit == 'true' + runs-on: ubuntu-latest + container: + image: ${{ needs.prepare.outputs.php-container }} + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + apk add --no-cache git + + # Install Xdebug for coverage + apk add --no-cache $PHPIZE_DEPS + pecl install xdebug + docker-php-ext-enable xdebug + + - uses: actions/download-artifact@v4 + with: + name: vendor-php${{ inputs.php-version }} + + - name: Extract vendor directory + run: tar -xzf vendor.tar.gz + + - name: Run PHPUnit tests + run: | + echo "๐Ÿงช Running PHPUnit tests with coverage..." + + # Check if PHPUnit is installed + if [ -f "vendor/bin/phpunit" ]; then + phpunit_cmd="vendor/bin/phpunit" + else + echo "โŒ PHPUnit not found in vendor/bin/" + exit 1 + fi + + # Build PHPUnit command with coverage + cmd="php -d memory_limit=${{ inputs.memory-limit }} -d xdebug.mode=coverage $phpunit_cmd" + cmd="$cmd --coverage-text --coverage-clover=coverage.xml" + + # Add debug flags if enabled + if [ "${{ inputs.debug }}" = "true" ]; then + cmd="$cmd --verbose" + fi + + # Execute PHPUnit + eval $cmd + + - name: Check coverage threshold + run: | + threshold=${{ inputs.coverage-threshold }} + if [ -f "coverage.xml" ] && [ "$threshold" != "0" ]; then + echo "๐Ÿ“Š Checking coverage threshold ($threshold%)..." + + # Extract coverage percentage from clover XML + coverage=$(php -r " + \$xml = simplexml_load_file('coverage.xml'); + \$metrics = \$xml->project->metrics; + \$covered = (float)\$metrics['coveredstatements']; + \$total = (float)\$metrics['statements']; + \$percentage = \$total > 0 ? (\$covered / \$total) * 100 : 0; + echo round(\$percentage, 2); + ") + + echo "๐Ÿ“Š Current coverage: $coverage%" + + # Compare with threshold + if (( $(echo "$coverage < $threshold" | bc -l) )); then + echo "โŒ Coverage $coverage% is below threshold $threshold%" + exit 1 + else + echo "โœ… Coverage $coverage% meets threshold $threshold%" + fi + else + echo "โ„น๏ธ Skipping coverage threshold check" + fi + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-php${{ inputs.php-version }} + path: coverage.xml + retention-days: 7 + + # Matrix testing across PHP versions + matrix-test: + name: ๐Ÿ”„ Matrix Testing + if: | + inputs.skip-tests == false && + needs.prepare.outputs.has-composer == 'true' && + needs.prepare.outputs.has-phpunit == 'true' + needs: [prepare, install] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ["8.1", "8.2", "8.3"] + exclude: + - php-version: ${{ inputs.php-version }} # Skip the version already tested above + container: + image: php:${{ matrix.php-version }}-cli-alpine + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: apk add --no-cache git unzip + + - name: Install Composer + run: | + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + - name: Install dependencies + run: | + php -d memory_limit=${{ inputs.memory-limit }} composer install \ + --no-progress \ + --no-suggest \ + --no-interaction \ + --optimize-autoloader \ + ${{ inputs.composer-args }} + + - name: Run quick tests + run: | + echo "๐Ÿงช Running tests on PHP ${{ matrix.php-version }}..." + if [ -f "vendor/bin/phpunit" ]; then + php -d memory_limit=${{ inputs.memory-limit }} vendor/bin/phpunit --no-coverage + else + echo "โš ๏ธ PHPUnit not found, skipping tests" + fi + + # Cleanup artifacts + cleanup: + name: ๐Ÿงน Cleanup + runs-on: ubuntu-latest + needs: [install, security-audit, phpstan, phpcs, phpmd, test, matrix-test] + if: always() + permissions: + actions: write + steps: + - name: Delete temporary artifacts + uses: actions/github-script@v7 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.GITHUB_RUN_ID, + }); + + for (const artifact of artifacts.data.artifacts) { + if (artifact.name.startsWith('vendor-php')) { + await github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: artifact.id + }); + core.info(`๐Ÿ—‘๏ธ Deleted artifact '${artifact.name}'`); + } + } \ No newline at end of file diff --git a/README.md b/README.md index 4dd93a1..38d9041 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,260 @@ A collection of GitHub action workflows. Built using the [reusable workflows](ht ## Workflows +### AWS CDK Deployment + +A comprehensive AWS CDK deployment workflow supporting multi-environment infrastructure deployments, stack management, and infrastructure validation with approval gates. + +#### **Features** +- **CDK synth โ†’ diff โ†’ deploy workflow**: Complete infrastructure deployment pipeline +- **Multi-environment support**: development, staging, and production deployments +- **Bootstrap validation**: Automatic CDK environment preparation and validation +- **Infrastructure validation**: Comprehensive stack validation and drift detection +- **Approval gates**: Manual approval workflows for production deployments +- **Changeset preview**: CloudFormation diff analysis before deployment +- **Rollback capabilities**: Support for stack destruction and rollback operations +- **Node.js optimization**: Configurable Node.js versions with dependency caching +- **Debug support**: Verbose logging and debug output for troubleshooting + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **Core Configuration** | +| aws-region | โŒ | string | ap-southeast-2 | AWS region for deployment | +| cdk-stack-name | โœ… | string | | CDK stack name to deploy (required) | +| environment-target | โŒ | string | development | Target environment (staging/production/development) | +| **Deployment Control** | +| approval-required | โŒ | boolean | true | Require manual approval before deployment | +| destroy-mode | โŒ | boolean | false | Destroy stack instead of deploying | +| bootstrap-stack | โŒ | boolean | false | Bootstrap CDK environment before deployment | +| **Node.js and CDK Configuration** | +| node-version | โŒ | string | 18 | Node.js version to use | +| cdk-version | โŒ | string | | Pin specific CDK version (optional) | +| **Advanced Configuration** | +| context-values | โŒ | string | {} | CDK context values as JSON object | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| aws-access-key-id | โœ… | AWS access key ID | +| aws-secret-access-key | โœ… | AWS secret access key | +| cfn-execution-role | โŒ | CloudFormation execution role ARN (optional, for cross-account deployments) | + +#### **Outputs** +| Name | Description | +|------|-------------| +| stack-outputs | CloudFormation stack outputs as JSON | +| deployment-status | Deployment status (success/failed/destroyed) | + +#### **Example Usage** + +**Basic Development Deployment:** +```yaml +jobs: + deploy-dev: + uses: aligent/workflows/.github/workflows/aws-cdk-deploy.yml@main + with: + cdk-stack-name: my-app-dev + environment-target: development + approval-required: false + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Production Deployment with Manual Approval:** +```yaml +jobs: + deploy-prod: + uses: aligent/workflows/.github/workflows/aws-cdk-deploy.yml@main + with: + cdk-stack-name: my-app-prod + environment-target: production + approval-required: true + node-version: "18" + debug: true + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cfn-execution-role: ${{ secrets.CFN_EXECUTION_ROLE }} +``` + +**Bootstrap New Environment:** +```yaml +jobs: + bootstrap-staging: + uses: aligent/workflows/.github/workflows/aws-cdk-deploy.yml@main + with: + cdk-stack-name: my-app-staging + environment-target: staging + bootstrap-stack: true + aws-region: us-east-1 + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Custom CDK Context and Version:** +```yaml +jobs: + deploy-custom: + uses: aligent/workflows/.github/workflows/aws-cdk-deploy.yml@main + with: + cdk-stack-name: my-app-custom + environment-target: staging + cdk-version: "2.100.0" + context-values: '{"vpc-id": "vpc-12345", "environment": "staging"}' + node-version: "20" + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Destroy Stack:** +```yaml +jobs: + destroy-stack: + uses: aligent/workflows/.github/workflows/aws-cdk-deploy.yml@main + with: + cdk-stack-name: my-app-old + environment-target: development + destroy-mode: true + approval-required: false + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +### Docker ECR Deployment + +A comprehensive Docker container deployment workflow supporting multi-platform builds, ECR registry management, vulnerability scanning, and container security with build optimization and registry lifecycle management. + +#### **Features** +- **Multi-platform builds**: Support for linux/amd64, linux/arm64, and ARM variants +- **ECR integration**: Automated ECR repository creation and lifecycle management +- **Vulnerability scanning**: Trivy security scanning with configurable thresholds +- **Container signing**: Optional cosign-based image signing and attestation +- **Smart tagging**: Multiple tagging strategies (latest, semantic, branch, custom) +- **Build optimization**: Advanced caching with registry and inline cache support +- **Registry cleanup**: Automated cleanup of old images with retention policies +- **Multi-stage builds**: Support for target build stages and build arguments +- **Security gates**: Configurable vulnerability thresholds blocking insecure deployments + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **Core Configuration** | +| aws-region | โŒ | string | ap-southeast-2 | AWS region for ECR registry | +| ecr-repository | โœ… | string | | ECR repository name (required) | +| dockerfile-path | โŒ | string | Dockerfile | Path to Dockerfile | +| build-context | โŒ | string | . | Docker build context path | +| **Platform and Build Configuration** | +| platforms | โŒ | string | linux/amd64,linux/arm64 | Target platforms for multi-platform builds | +| push-to-registry | โŒ | boolean | true | Push built images to ECR registry | +| **Security and Scanning** | +| vulnerability-scan | โŒ | boolean | true | Enable container vulnerability scanning | +| security-threshold | โŒ | string | HIGH | Security vulnerability threshold (CRITICAL/HIGH/MEDIUM/LOW) | +| **Tagging Strategy** | +| tag-strategy | โŒ | string | latest | Image tagging strategy (latest/semantic/branch/custom) | +| custom-tags | โŒ | string | | Custom tags (comma-separated) when using custom strategy | +| **Build Optimization** | +| cache-from | โŒ | string | | Cache sources for build optimization (comma-separated) | +| build-args | โŒ | string | {} | Docker build arguments as JSON object | +| target-stage | โŒ | string | | Target build stage for multi-stage Dockerfiles | +| **Registry Management** | +| cleanup-old-images | โŒ | boolean | false | Clean up old images from ECR registry | +| retention-count | โŒ | string | 10 | Number of images to retain when cleaning up | +| **Container Signing** | +| enable-signing | โŒ | boolean | false | Enable container image signing with cosign | +| **Advanced Configuration** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| aws-access-key-id | โœ… | AWS access key ID | +| aws-secret-access-key | โœ… | AWS secret access key | +| container-signing-key | โŒ | Private key for container signing (optional) | + +#### **Outputs** +| Name | Description | +|------|-------------| +| image-uri | Full URI of the built container image | +| image-digest | SHA256 digest of the built image | +| image-tags | Applied image tags as JSON array | +| vulnerability-report | Container vulnerability scan results | + +#### **Example Usage** + +**Basic Docker Build and Push:** +```yaml +jobs: + docker-deploy: + uses: aligent/workflows/.github/workflows/docker-ecr-deploy.yml@main + with: + ecr-repository: my-app + dockerfile-path: Dockerfile + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Multi-platform with Vulnerability Scanning:** +```yaml +jobs: + secure-deploy: + uses: aligent/workflows/.github/workflows/docker-ecr-deploy.yml@main + with: + ecr-repository: my-secure-app + platforms: "linux/amd64,linux/arm64" + vulnerability-scan: true + security-threshold: "CRITICAL" + tag-strategy: "semantic" + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Production with Signing and Cleanup:** +```yaml +jobs: + production-deploy: + uses: aligent/workflows/.github/workflows/docker-ecr-deploy.yml@main + with: + ecr-repository: my-prod-app + tag-strategy: "semantic" + enable-signing: true + cleanup-old-images: true + retention-count: "5" + security-threshold: "HIGH" + aws-region: "us-east-1" + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + container-signing-key: ${{ secrets.COSIGN_PRIVATE_KEY }} +``` + +**Custom Build with Optimization:** +```yaml +jobs: + optimized-build: + uses: aligent/workflows/.github/workflows/docker-ecr-deploy.yml@main + with: + ecr-repository: my-optimized-app + build-context: "./backend" + dockerfile-path: "./backend/Dockerfile.prod" + target-stage: "production" + build-args: '{"NODE_ENV": "production", "API_VERSION": "v2"}' + cache-from: "my-optimized-app:buildcache,my-base-image:latest" + tag-strategy: "custom" + custom-tags: "latest,v2.1.0,production" + debug: true + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + ### Node Pull Request Checks #### **Inputs** @@ -32,6 +286,117 @@ jobs: skip-format: false ``` +### Magento Cloud Deployment + +A comprehensive Magento Cloud deployment workflow supporting multi-environment deployments, ECE patches, dependency injection compilation, NewRelic monitoring, and production approval gates. + +#### **Features** +- **Multi-environment support**: integration, staging, and production deployments +- **PHP 8.1-8.3 support**: Magento-optimized container environments +- **ECE patches integration**: Automatic application of Magento Cloud patches +- **DI compilation**: Memory-optimized dependency injection compilation +- **NewRelic integration**: Deployment markers and performance monitoring +- **Production gates**: Manual approval workflow for production deployments +- **CST system integration**: Version reporting to centralized tracking systems +- **Full git history support**: Required for Magento Cloud deployment requirements +- **Health monitoring**: Post-deployment verification and performance checks + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **Magento Cloud Configuration** | +| magento-cloud-project-id | โœ… | string | | Magento Cloud project ID (required) | +| environment | โŒ | string | integration | Target environment (integration/staging/production) | +| **PHP Configuration** | +| php-version | โŒ | string | 8.1 | PHP version for Magento (8.1, 8.2, 8.3) | +| memory-limit | โŒ | string | -1 | PHP memory limit for compilation (-1 for unlimited) | +| **Magento-specific Configuration** | +| apply-patches | โŒ | boolean | true | Apply ECE patches before deployment | +| di-compile | โŒ | boolean | true | Run dependency injection compilation | +| **Deployment Control** | +| manual-deploy | โŒ | boolean | false | Require manual approval for production deployments | +| **Monitoring and Reporting** | +| newrelic-app-id | โŒ | string | | NewRelic application ID for deployment markers (optional) | +| **Advanced Configuration** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| magento-cloud-cli-token | โœ… | Magento Cloud CLI token for authentication | +| newrelic-api-key | โŒ | NewRelic API key for deployment markers (optional) | +| cst-reporting-token | โŒ | CST system reporting token (optional) | + +#### **Outputs** +| Name | Description | +|------|-------------| +| deployment-url | URL of the deployed Magento application | +| deployment-id | Magento Cloud deployment ID | + +#### **Example Usage** + +**Basic Integration Deployment:** +```yaml +jobs: + deploy-integration: + uses: aligent/workflows/.github/workflows/magento-cloud-deploy.yml@main + with: + magento-cloud-project-id: abc123def456 + environment: integration + php-version: "8.1" + secrets: + magento-cloud-cli-token: ${{ secrets.MAGENTO_CLOUD_CLI_TOKEN }} +``` + +**Production Deployment with Manual Approval:** +```yaml +jobs: + deploy-production: + uses: aligent/workflows/.github/workflows/magento-cloud-deploy.yml@main + with: + magento-cloud-project-id: abc123def456 + environment: production + php-version: "8.2" + manual-deploy: true + newrelic-app-id: "123456789" + secrets: + magento-cloud-cli-token: ${{ secrets.MAGENTO_CLOUD_CLI_TOKEN }} + newrelic-api-key: ${{ secrets.NEWRELIC_API_KEY }} + cst-reporting-token: ${{ secrets.CST_REPORTING_TOKEN }} +``` + +**Staging Deployment with Custom PHP and Debug:** +```yaml +jobs: + deploy-staging: + uses: aligent/workflows/.github/workflows/magento-cloud-deploy.yml@main + with: + magento-cloud-project-id: abc123def456 + environment: staging + php-version: "8.3" + memory-limit: "4G" + debug: true + apply-patches: true + di-compile: true + secrets: + magento-cloud-cli-token: ${{ secrets.MAGENTO_CLOUD_CLI_TOKEN }} +``` + +**Skip ECE Patches and DI Compilation:** +```yaml +jobs: + deploy-fast: + uses: aligent/workflows/.github/workflows/magento-cloud-deploy.yml@main + with: + magento-cloud-project-id: abc123def456 + environment: integration + apply-patches: false + di-compile: false + debug: true + secrets: + magento-cloud-cli-token: ${{ secrets.MAGENTO_CLOUD_CLI_TOKEN }} +``` + ### Nx Serverless Deployment #### **Inputs** @@ -64,3 +429,356 @@ jobs: aws-access-key-id: '123' aws-secret-access-key: '456' ``` + +### PHP Quality Checks + +A comprehensive PHP quality assurance workflow supporting static analysis, coding standards validation, security auditing, and testing with coverage reporting across multiple PHP versions. + +#### **Features** +- **PHPStan static analysis**: Configurable levels (1-9) with intelligent configuration detection +- **PHP CodeSniffer**: Support for Magento2, PSR12, and PSR2 coding standards +- **Composer security audit**: Automated vulnerability scanning of dependencies +- **PHPUnit testing**: Full test suite execution with coverage threshold enforcement +- **PHPMD mess detection**: Code quality analysis for maintainability issues +- **Multi-PHP support**: Matrix testing across PHP 8.1, 8.2, and 8.3 +- **Smart caching**: Optimized Composer and analysis result caching +- **Parallel execution**: Concurrent quality checks for maximum efficiency +- **Flexible configuration**: Skip individual checks and customize tool behavior + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **PHP Configuration** | +| php-version | โŒ | string | 8.1 | PHP version to use (8.1, 8.2, 8.3) | +| memory-limit | โŒ | string | 512M | PHP memory limit for analysis tools | +| **PHPStan Configuration** | +| phpstan-level | โŒ | string | 6 | PHPStan analysis level (1-9) | +| skip-phpstan | โŒ | boolean | false | Skip PHPStan static analysis | +| **Code Style Configuration** | +| coding-standard | โŒ | string | Magento2 | Coding standard (Magento2, PSR12, PSR2) | +| skip-phpcs | โŒ | boolean | false | Skip PHP CodeSniffer checks | +| **Testing Configuration** | +| coverage-threshold | โŒ | string | 80 | Code coverage threshold percentage (0-100) | +| skip-tests | โŒ | boolean | false | Skip PHP unit testing | +| **Composer Configuration** | +| composer-args | โŒ | string | | Additional composer install arguments | +| **Advanced Configuration** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Example Usage** + +**Basic Quality Checks:** +```yaml +jobs: + quality-check: + uses: aligent/workflows/.github/workflows/php-quality-checks.yml@main + with: + php-version: "8.2" + phpstan-level: "7" +``` + +**Magento 2 Project with Custom Standards:** +```yaml +jobs: + magento-quality: + uses: aligent/workflows/.github/workflows/php-quality-checks.yml@main + with: + php-version: "8.1" + coding-standard: "Magento2" + phpstan-level: "6" + coverage-threshold: "75" + memory-limit: "1G" + debug: true +``` + +**Skip Specific Checks:** +```yaml +jobs: + custom-checks: + uses: aligent/workflows/.github/workflows/php-quality-checks.yml@main + with: + php-version: "8.3" + skip-phpcs: true + skip-tests: true + phpstan-level: "9" +``` + +**PSR Standards with High Coverage:** +```yaml +jobs: + strict-quality: + uses: aligent/workflows/.github/workflows/php-quality-checks.yml@main + with: + php-version: "8.2" + coding-standard: "PSR12" + phpstan-level: "8" + coverage-threshold: "90" + composer-args: "--no-dev" +``` + +### CLI Tools Release + +A comprehensive CLI tools release workflow supporting OCLIF packaging, multi-platform compilation, S3-based distribution, GPG signing, Homebrew formula generation, and self-updating CLI capabilities with automated version management. + +#### **Features** +- **OCLIF packaging**: Complete CLI bundling and optimization for multiple architectures +- **Multi-platform builds**: Support for linux, darwin, windows across x64 and arm64 +- **S3 distribution**: Automated package distribution with versioned and latest channels +- **GPG signing**: Optional cryptographic signing for release integrity and security +- **Homebrew integration**: Automatic formula generation and tap repository updates +- **Self-updating support**: Update manifest generation for CLI auto-update capabilities +- **Version strategies**: Semantic versioning, git tags, and custom versioning support +- **GitHub releases**: Automated release creation with comprehensive release notes +- **Security validation**: Input validation and secure credential handling +- **Debug support**: Verbose logging and comprehensive error reporting + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **Core Configuration** | +| tool-name | โœ… | string | | CLI tool name (alphanumeric, hyphens, underscores only) | +| version-strategy | โŒ | string | semantic | Version strategy (semantic/tag/custom) | +| platforms | โŒ | string | linux-x64,darwin-x64,darwin-arm64 | Target platforms for compilation | +| **Distribution Configuration** | +| s3-bucket | โœ… | string | | S3 bucket for distribution (required) | +| s3-path | โŒ | string | | S3 path prefix for distribution | +| homebrew-tap | โŒ | string | | Homebrew tap repository (format: owner/repo) | +| **Security Configuration** | +| gpg-sign | โŒ | boolean | true | Enable GPG signing for release integrity | +| **Release Configuration** | +| create-release | โŒ | boolean | true | Create GitHub release with assets | +| release-notes | โŒ | string | | Custom release notes (optional) | +| **Technical Configuration** | +| node-version | โŒ | string | 18 | Node.js version for OCLIF environment | +| oclif-version | โŒ | string | | Pin specific OCLIF version (optional) | +| aws-region | โŒ | string | ap-southeast-2 | AWS region for S3 distribution | +| **Advanced Configuration** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| aws-access-key-id | โœ… | AWS access key ID for S3 distribution | +| aws-secret-access-key | โœ… | AWS secret access key for S3 distribution | +| gpg-private-key | โŒ | GPG private key for signing (recommended when gpg-sign enabled) | +| gpg-passphrase | โŒ | GPG passphrase for signing (recommended when gpg-sign enabled) | +| homebrew-github-token | โŒ | GitHub token for Homebrew tap updates (required for Homebrew) | + +#### **Outputs** +| Name | Description | +|------|-------------| +| release-version | Released version identifier | +| release-url | GitHub release URL | +| distribution-urls | Distribution URLs as JSON object with platform mappings | +| homebrew-formula | Homebrew formula update status | + +#### **Example Usage** + +**Basic CLI Release:** +```yaml +jobs: + release-cli: + uses: aligent/workflows/.github/workflows/cli-tools-release.yml@main + with: + tool-name: my-awesome-cli + s3-bucket: my-distribution-bucket + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Production Release with Signing and Homebrew:** +```yaml +jobs: + release-production: + uses: aligent/workflows/.github/workflows/cli-tools-release.yml@main + with: + tool-name: production-cli + version-strategy: semantic + platforms: "linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64" + s3-bucket: cli-releases-bucket + s3-path: "releases" + homebrew-tap: "myorg/homebrew-tools" + gpg-sign: true + create-release: true + node-version: "18" + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} + homebrew-github-token: ${{ secrets.HOMEBREW_TOKEN }} +``` + +**Custom Version with Debug:** +```yaml +jobs: + release-custom: + uses: aligent/workflows/.github/workflows/cli-tools-release.yml@main + with: + tool-name: beta-cli + version-strategy: custom + platforms: "linux-x64,darwin-x64" + s3-bucket: beta-releases + gpg-sign: false + create-release: true + debug: true + release-notes: | + Beta release with experimental features. + + โš ๏ธ This is a pre-release version for testing purposes. + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Cross-platform with Specific OCLIF Version:** +```yaml +jobs: + release-cross-platform: + uses: aligent/workflows/.github/workflows/cli-tools-release.yml@main + with: + tool-name: cross-platform-cli + version-strategy: tag + platforms: "linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64,win32-arm64" + s3-bucket: universal-cli-bucket + s3-path: "tools" + oclif-version: "3.0.0" + aws-region: "us-east-1" + gpg-sign: true + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} +``` + +### BigCommerce Theme Deployment + +A comprehensive BigCommerce Stencil theme deployment workflow supporting theme bundling, environment promotion, asset optimization, backup/restore capabilities, and multi-environment deployment with comprehensive validation. + +#### **Features** +- **Stencil CLI integration**: Complete theme bundling, validation, and deployment pipeline +- **Multi-environment support**: Staging and production deployment workflows +- **Theme validation**: Bundle size checks, file permissions, and configuration validation +- **Asset optimization**: CSS/JS compression, image optimization, and bundle optimization +- **Backup & recovery**: Automatic current theme backup with rollback capabilities +- **Version management**: Theme versioning and deployment tracking +- **Environment templating**: Configuration management across environments +- **Security validation**: Theme structure validation and dependency auditing +- **Channel management**: Support for multi-channel theme deployment +- **Debug support**: Verbose logging and comprehensive error reporting + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **Core Configuration** | +| store-hash | โœ… | string | | BigCommerce store hash (10 character alphanumeric) | +| environment | โŒ | string | staging | Target environment (staging/production) | +| theme-name | โœ… | string | | Theme name for identification | +| **Deployment Control** | +| activate-theme | โŒ | boolean | true | Activate theme after successful deployment | +| bundle-optimization | โŒ | boolean | true | Enable theme asset optimization and compression | +| backup-current | โŒ | boolean | true | Backup current theme before deployment | +| **Technical Configuration** | +| node-version | โŒ | string | 18 | Node.js version for Stencil CLI environment | +| stencil-version | โŒ | string | | Pin specific Stencil CLI version (optional) | +| theme-config | โŒ | string | | Theme configuration as JSON object (optional) | +| **Theme Management** | +| variation-name | โŒ | string | | Specific theme variation to activate (optional) | +| channel-ids | โŒ | string | | Channel IDs for theme application (comma-separated) | +| apply-to-all-channels | โŒ | boolean | false | Apply theme to all store channels | +| delete-oldest | โŒ | boolean | false | Delete oldest theme to make room for new deployment | +| **Advanced Configuration** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| bigcommerce-access-token | โœ… | BigCommerce API access token with theme modify scope | +| bigcommerce-client-id | โœ… | BigCommerce API client ID | +| bigcommerce-client-secret | โœ… | BigCommerce API client secret | + +#### **Outputs** +| Name | Description | +|------|-------------| +| theme-uuid | Deployed theme UUID from BigCommerce | +| theme-version | Deployed theme version identifier | +| deployment-url | BigCommerce store URL for theme verification | +| backup-created | Whether current theme backup was created | + +#### **Example Usage** + +**Basic Staging Deployment:** +```yaml +jobs: + deploy-staging: + uses: aligent/workflows/.github/workflows/bigcommerce-theme-deploy.yml@main + with: + store-hash: "abc123def4" + environment: staging + theme-name: "my-storefront-theme" + secrets: + bigcommerce-access-token: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }} + bigcommerce-client-id: ${{ secrets.BIGCOMMERCE_CLIENT_ID }} + bigcommerce-client-secret: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }} +``` + +**Production Deployment with Custom Configuration:** +```yaml +jobs: + deploy-production: + uses: aligent/workflows/.github/workflows/bigcommerce-theme-deploy.yml@main + with: + store-hash: "xyz789abc1" + environment: production + theme-name: "my-production-theme" + activate-theme: true + bundle-optimization: true + backup-current: true + variation-name: "Desktop" + stencil-version: "6.15.0" + node-version: "18" + secrets: + bigcommerce-access-token: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }} + bigcommerce-client-id: ${{ secrets.BIGCOMMERCE_CLIENT_ID }} + bigcommerce-client-secret: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }} +``` + +**Multi-Channel Deployment:** +```yaml +jobs: + deploy-multi-channel: + uses: aligent/workflows/.github/workflows/bigcommerce-theme-deploy.yml@main + with: + store-hash: "def456ghi7" + environment: staging + theme-name: "multi-channel-theme" + apply-to-all-channels: true + delete-oldest: true + theme-config: '{"logo": {"url": "https://cdn.example.com/logo.png"}, "colors": {"primary": "#ff6b35"}}' + debug: true + secrets: + bigcommerce-access-token: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }} + bigcommerce-client-id: ${{ secrets.BIGCOMMERCE_CLIENT_ID }} + bigcommerce-client-secret: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }} +``` + +**Specific Channel Deployment:** +```yaml +jobs: + deploy-specific-channels: + uses: aligent/workflows/.github/workflows/bigcommerce-theme-deploy.yml@main + with: + store-hash: "ghi789jkl0" + environment: production + theme-name: "channel-specific-theme" + channel-ids: "1,2,5" + variation-name: "Mobile" + bundle-optimization: false + backup-current: false + secrets: + bigcommerce-access-token: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }} + bigcommerce-client-id: ${{ secrets.BIGCOMMERCE_CLIENT_ID }} + bigcommerce-client-secret: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }} +```