diff --git a/.github/workflows/app-smoke-test.yml b/.github/workflows/app-smoke-test.yml index 1b4902e..f1f7727 100644 --- a/.github/workflows/app-smoke-test.yml +++ b/.github/workflows/app-smoke-test.yml @@ -8,7 +8,7 @@ on: required: false default: 'master' schedule: - # run daily at midnight + # Run daily at midnight UTC - cron: '15 0 * * *' jobs: @@ -17,27 +17,36 @@ jobs: strategy: max-parallel: 1 matrix: - node-version: [18.x, 20.x] + node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest] steps: + - name: Checkout e2e-tests repo (for scripts) + uses: actions/checkout@v3 + with: + path: e2e-tests + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - name: Setup CLI uses: actions/checkout@v3 with: repository: adobe/aio-cli-plugin-app ref: ${{ github.event.inputs.branch-name }} + path: aio-cli-plugin-app + - name: Update package.json oclif.plugins uses: jossef/action-set-json-field@v2.1 with: - file: package.json + file: aio-cli-plugin-app/package.json field: oclif.plugins value: "[\"@adobe/aio-cli-plugin-app-templates\"]" parse_json: true + - name: Auth - uses: adobe/aio-apps-action@3.3.0 + uses: adobe/aio-apps-action@3.3.1 with: os: ${{ matrix.os }} command: oauth_sts @@ -49,69 +58,48 @@ jobs: # see https://github.com/adobe/aio-lib-ims-oauth/issues/114 TECHNICALACCOUNTID: dummy_id TECHNICALACCOUNTEMAIL: dummy_email - - id: create - name: create app with no extensions - run: | - npm i @adobe/aio-cli-plugin-app-templates - npm i - mkdir ffapp - cd ffapp - ../bin/run app:init . -y --no-login --standalone-app > consoleoutput.txt - grep "App initialization finished" consoleoutput.txt - - echo AIO_RUNTIME_AUTH=${{secrets.RUNTIME_AUTH}} > .env - echo AIO_RUNTIME_NAMESPACE=${{secrets.RUNTIME_NAMESPACE}} >> .env + - id: create + name: Create app with no extensions + working-directory: aio-cli-plugin-app + run: node ../e2e-tests/smoke-tests/create-app.js + env: + AIO_RUNTIME_AUTH: ${{ secrets.RUNTIME_AUTH }} + AIO_RUNTIME_NAMESPACE: ${{ secrets.RUNTIME_NAMESPACE }} - ../bin/run app:deploy >> consoleoutput.txt - grep "Your deployed actions:" consoleoutput.txt - grep "api/v1/web/ffapp/generic" consoleoutput.txt - grep "/api/v1/web/ffapp/publish-events" consoleoutput.txt - grep ".adobeio-static.net/index.html" consoleoutput.txt - grep "Successful deployment" consoleoutput.txt - id: app_pack - name: pack an app (uses previous created app) - run: | - cd ffapp + name: Pack an app (uses previous created app) + working-directory: aio-cli-plugin-app + run: node ../e2e-tests/smoke-tests/pack.js - ../bin/run app:pack &> consoleoutput.txt - grep "Packaging done." consoleoutput.txt - id: app_install - name: install an app (uses previous packed app) - run: | - cd ffapp + name: Install an app (uses previous packed app) + working-directory: aio-cli-plugin-app + run: node ../e2e-tests/smoke-tests/install.js - ../bin/run app:install dist/app.zip --output install-folder &> consoleoutput.txt - grep "Install done." consoleoutput.txt - id: createext - name: create app with extension - run: | - rm -rf ffapp - mkdir ffapp - cd ffapp - - ../bin/run app:init . -y --no-login --extension dx/excshell/1 > consoleoutput.txt - grep "App initialization finished" consoleoutput.txt - - echo AIO_RUNTIME_AUTH=${{secrets.RUNTIME_AUTH}} > .env - echo AIO_RUNTIME_NAMESPACE=${{secrets.RUNTIME_NAMESPACE}} >> .env - - ../bin/run app:deploy --no-publish >> consoleoutput.txt - grep "Your deployed actions:" consoleoutput.txt - grep "api/v1/web/dx-excshell-1/generic" consoleoutput.txt - grep ".adobeio-static.net/index.html" consoleoutput.txt - grep "Successful deployment" consoleoutput.txt + name: Create app with extension + working-directory: aio-cli-plugin-app + run: node ../e2e-tests/smoke-tests/create-app-extension.js + env: + AIO_RUNTIME_AUTH: ${{ secrets.RUNTIME_AUTH }} + AIO_RUNTIME_NAMESPACE: ${{ secrets.RUNTIME_NAMESPACE }} - id: output name: Write the output to console for debugging if: ${{ failure() }} + working-directory: aio-cli-plugin-app run: | - if [ -d "ffapp" ]; then cd ffapp; fi - cat consoleoutput.txt + if [ -d "ffapp" ]; then + echo "=== Console output from ffapp/consoleoutput.txt ===" + cat ffapp/consoleoutput.txt + else + echo "ffapp directory not found" + fi - id: slacknotification name: Slack Notification - if: ${{ failure() }} + if: ${{ failure() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/app-test.yml b/.github/workflows/app-test.yml index 1e8f339..6868d3b 100644 --- a/.github/workflows/app-test.yml +++ b/.github/workflows/app-test.yml @@ -11,13 +11,13 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [18] + node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm i --package-lock --package-lock-only @@ -48,7 +48,7 @@ jobs: run: | git config user.name adobe-bot git config user.email grp-opensourceoffice@adobe.com - echo "- $(date) Node ${{ matrix.node-version }} ${{ job.status == 'success' && '🎉 success' || job.status }}" >> logs/run.md + echo "- $(date) Node ${{ matrix.node-version }} ${{ job.status == 'success' && 'success' || job.status }}" >> logs/run.md git add logs/run.md git commit -m "generated" git push diff --git a/.github/workflows/asset-compute-smoke-test.yml b/.github/workflows/asset-compute-smoke-test.yml index d6c3ebc..d3fd5c5 100644 --- a/.github/workflows/asset-compute-smoke-test.yml +++ b/.github/workflows/asset-compute-smoke-test.yml @@ -2,6 +2,7 @@ name: Asset Compute smoke test (init, build and deploy) on: workflow_dispatch: + pull_request: schedule: # run daily at midnight - cron: '15 0 * * *' @@ -12,36 +13,45 @@ jobs: strategy: max-parallel: 1 matrix: - node-version: [18.x, 20.x] + node-version: [20.x, 22.x, 24.x] os: [ubuntu-latest] steps: + - name: Checkout e2e-tests repo (for scripts) + uses: actions/checkout@v3 + with: + path: e2e-tests + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - name: Setup Asset Compute uses: actions/checkout@master with: repository: adobe/asset-compute-integration-tests + path: asset-compute-tests + - id: create-asset-compute name: asset compute smoke integration test - run: | - npm i - ./node_modules/mocha/bin/mocha test/index.test.js > consoleoutput.txt - - grep "App initialization finished!" consoleoutput.txt - grep -e "Running tests in .*/test/asset-compute/worker" consoleoutput.txt - grep "All tests were successful." consoleoutput.txt + working-directory: asset-compute-tests + run: node ../e2e-tests/smoke-tests/asset-compute.js - id: output name: Write the output to console for debugging if: ${{ failure() }} + working-directory: asset-compute-tests run: | - cat consoleoutput.txt + if [ -f "consoleoutput.txt" ]; then + echo "=== Console output from consoleoutput.txt ===" + cat consoleoutput.txt + else + echo "consoleoutput.txt not found" + fi - id: slacknotification name: Slack Notification - if: ${{ failure() }} + if: ${{ failure() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/README.md b/README.md index 92fc8ee..21b7a46 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,30 @@ E2e tests for Adobe I/O SDKs and tools. Each tested repository has its own requirements, mostly environment variables to be set. Tests are run against [an internal Adobe Console Project](https://developer.adobe.com/console/projects/53444/4566206088344607082). -## Run +## Running Tests -`npm run all` +### Full E2E Test Suite + +Run all e2e tests for all libraries: +```bash +npm run all +``` + +This executes `src/index.js` which runs e2e tests for all configured repositories. + +### Individual Smoke Tests (GitHub Actions) + +The smoke tests for `aio-cli-plugin-app` are run via GitHub Actions workflows using JavaScript scripts: + +- **App Plugin Smoke Test**: `.github/workflows/app-smoke-test.yml` + - Uses scripts from `.github/scripts/smoke-test.js` + - Tests: create app, pack, install, create with extension + +- **Asset Compute Smoke Test**: `.github/workflows/asset-compute-smoke-test.yml` + - Uses scripts from `.github/scripts/smoke-test.js` + - Tests: Asset Compute integration + +These workflows can also be run locally (see `.github/scripts/README.md` for details). ## Tests diff --git a/smoke-tests/README.md b/smoke-tests/README.md new file mode 100644 index 0000000..d010a81 --- /dev/null +++ b/smoke-tests/README.md @@ -0,0 +1,51 @@ +# Smoke Tests + +Individual test scripts for AIO workflows, replacing inline shell code in GitHub Actions. + +## Test Scripts + +Each script is standalone and can be run directly: + +- `create-app.js` - Create and deploy app with no extensions +- `create-app-extension.js` - Create and deploy app with extension +- `pack.js` - Package app into a zip file +- `install.js` - Install app from zip file +- `asset-compute.js` - Run Asset Compute integration tests + +## Usage + +### In GitHub Actions Workflows + +```yaml +- name: Create app with no extensions + working-directory: aio-cli-plugin-app + run: node ../e2e-tests/smoke-tests/create-app.js + env: + AIO_RUNTIME_AUTH: ${{ secrets.RUNTIME_AUTH }} + AIO_RUNTIME_NAMESPACE: ${{ secrets.RUNTIME_NAMESPACE }} +``` + +### Local Testing + +```bash +# Test with mocks (no credentials required) +cd smoke-tests +node test-harness.js + +# Test with actual CLI (requires credentials) +export AIO_RUNTIME_AUTH="your-auth" +export AIO_RUNTIME_NAMESPACE="your-namespace" +node create-app.js +``` + +## Files + +- `utils.js` - Shared utilities (runCommand, verifyOutput, createAndDeployApp, etc.) +- `test-harness.js` - Local testing with mocks (no credentials needed) +- `tests/fixtures/` - Mock output files for test-harness.js + +## Workflows + +These scripts are used by: +- `.github/workflows/app-smoke-test.yml` +- `.github/workflows/asset-compute-smoke-test.yml` diff --git a/smoke-tests/asset-compute.js b/smoke-tests/asset-compute.js new file mode 100644 index 0000000..077e394 --- /dev/null +++ b/smoke-tests/asset-compute.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +/** + * Asset Compute smoke test + */ + +const { runCommand, verifyOutput } = require('./utils'); + +const outputFile = 'consoleoutput.txt'; + +try { + console.log('=== Step 1: Install dependencies ==='); + runCommand('npm i'); + + console.log('\n=== Step 2: Run Asset Compute tests ==='); + runCommand('./node_modules/mocha/bin/mocha test/index.test.js > consoleoutput.txt'); + + console.log('\n=== Step 3: Verify test results ==='); + verifyOutput(outputFile, [ + 'App initialization finished!', + '/test/asset-compute/worker', + 'All tests were successful.' + ]); + + console.log('\nAsset Compute smoke test passed!'); +} catch (error) { + console.error('\nScript failed:', error.message); + throw error; +} diff --git a/smoke-tests/create-app-extension.js b/smoke-tests/create-app-extension.js new file mode 100644 index 0000000..110b571 --- /dev/null +++ b/smoke-tests/create-app-extension.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Create and deploy app with extension + */ + +const { createAndDeployApp } = require('./utils'); + +createAndDeployApp({ + installDeps: false, + cleanFirst: true, + initCommand: 'app:init . -y --no-login --extension dx/excshell/1', + deployCommand: 'app:deploy --no-publish', + verifyStrings: [ + 'Your deployed actions:', + 'api/v1/web/dx-excshell-1/generic', + '.adobeio-static.net/index.html', + 'Successful deployment' + ], + successMessage: 'App with extension creation and deployment successful!' +}); diff --git a/smoke-tests/create-app.js b/smoke-tests/create-app.js new file mode 100644 index 0000000..40d9cb0 --- /dev/null +++ b/smoke-tests/create-app.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +/** + * Create and deploy app with no extensions + */ + +const { createAndDeployApp } = require('./utils'); + +createAndDeployApp({ + installDeps: true, + cleanFirst: false, + initCommand: 'app:init . -y --no-login --standalone-app', + deployCommand: 'app:deploy', + verifyStrings: [ + 'Your deployed actions:', + 'api/v1/web/ffapp/generic', + '/api/v1/web/ffapp/publish-events', + '.adobeio-static.net/index.html', + 'Successful deployment' + ], + successMessage: 'App creation and deployment successful!' +}); diff --git a/smoke-tests/install.js b/smoke-tests/install.js new file mode 100644 index 0000000..738d61a --- /dev/null +++ b/smoke-tests/install.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Install app from zip file + */ + +const { runCommand, verifyOutput } = require('./utils'); + +const appDir = 'ffapp'; +const binPath = '../bin/run'; + +try { + console.log('=== Installing app ==='); + process.chdir(appDir); + runCommand(`${binPath} app:install dist/app.zip --output install-folder 2>&1 > consoleoutput.txt`); + verifyOutput('consoleoutput.txt', ['Install done.']); + console.log('\nApp installation successful!'); +} catch (error) { + console.error('\nScript failed:', error.message); + throw error; +} diff --git a/smoke-tests/pack.js b/smoke-tests/pack.js new file mode 100644 index 0000000..46701ca --- /dev/null +++ b/smoke-tests/pack.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Pack app into a zip file + */ + +const { runCommand, verifyOutput } = require('./utils'); + +const appDir = 'ffapp'; +const binPath = '../bin/run'; + +try { + console.log('=== Packing app ==='); + process.chdir(appDir); + runCommand(`${binPath} app:pack 2>&1 > consoleoutput.txt`); + verifyOutput('consoleoutput.txt', ['Packaging done.']); + console.log('\nApp packing successful!'); +} catch (error) { + console.error('\nScript failed:', error.message); + throw error; +} diff --git a/smoke-tests/test-harness.js b/smoke-tests/test-harness.js new file mode 100644 index 0000000..5a7de99 --- /dev/null +++ b/smoke-tests/test-harness.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node + +/** + * Test harness for validating scripts locally + * This allows testing the scripts without needing GitHub Actions or Adobe secrets + */ + +const fs = require('fs'); +const path = require('path'); +const { verifyOutput } = require('./utils'); + +// Path to test fixtures +const fixturesDir = path.join(__dirname, 'tests', 'fixtures'); + +/** + * Test verification functions + */ +function testVerifications() { + console.log('=== Testing Verification Functions ===\n'); + + const tests = [ + { + name: 'App initialization', + file: path.join(fixturesDir, 'init-output.txt'), + expected: ['App initialization finished'] + }, + { + name: 'App deployment (standalone)', + file: path.join(fixturesDir, 'deploy-standalone.txt'), + expected: [ + 'Your deployed actions:', + 'api/v1/web/ffapp/generic', + '/api/v1/web/ffapp/publish-events', + '.adobeio-static.net/index.html', + 'Successful deployment' + ] + }, + { + name: 'App deployment (with extension)', + file: path.join(fixturesDir, 'deploy-extension.txt'), + expected: [ + 'Your deployed actions:', + 'api/v1/web/dx-excshell-1/generic', + '.adobeio-static.net/index.html', + 'Successful deployment' + ] + }, + { + name: 'App packing', + file: path.join(fixturesDir, 'pack-output.txt'), + expected: ['Packaging done.'] + }, + { + name: 'App installation', + file: path.join(fixturesDir, 'install-output.txt'), + expected: ['Install done.'] + }, + { + name: 'Asset Compute smoke test', + file: path.join(fixturesDir, 'asset-compute-output.txt'), + expected: [ + 'App initialization finished!', + '/test/asset-compute/worker', + 'All tests were successful.' + ] + } + ]; + + let passed = 0; + let failed = 0; + + for (const test of tests) { + try { + console.log(`Testing: ${test.name}`); + verifyOutput(test.file, test.expected); + console.log('PASSED\n'); + passed++; + } catch (error) { + console.error(`FAILED: ${error.message}\n`); + failed++; + } + } + + console.log('=== Test Summary ==='); + console.log(`Passed: ${passed}/${tests.length}`); + console.log(`Failed: ${failed}/${tests.length}`); + + return failed === 0; +} + +/** + * Test with missing strings (should fail) + */ +function testNegativeCases() { + console.log('\n=== Testing Negative Cases (Expected to Fail) ===\n'); + + try { + console.log('Testing: Missing expected string'); + verifyOutput(path.join(fixturesDir, 'init-output.txt'), ['This string does not exist']); + console.log('UNEXPECTED: Test should have failed\n'); + return false; + } catch (error) { + console.log('EXPECTED: Test failed as expected\n'); + return true; + } +} + +/** + * Main test runner + */ +function main() { + console.log('Running Test Harness\n'); + console.log('This tests the verification logic without needing actual CLI execution\n'); + + const positiveTestsPassed = testVerifications(); + const negativeTestsPassed = testNegativeCases(); + + if (positiveTestsPassed && negativeTestsPassed) { + console.log('\nAll tests passed!'); + console.log('\nNext steps:'); + console.log('1. Review the fixture files in smoke-tests/tests/fixtures/'); + console.log('2. Test with actual CLI once you have Adobe credentials'); + } else { + console.error('\nSome tests failed'); + throw new Error('Test harness failed'); + } +} + +// Run the tests +if (require.main === module) { + main(); +} + +module.exports = { testVerifications, testNegativeCases }; diff --git a/smoke-tests/tests/fixtures/asset-compute-output.txt b/smoke-tests/tests/fixtures/asset-compute-output.txt new file mode 100644 index 0000000..c6d66f0 --- /dev/null +++ b/smoke-tests/tests/fixtures/asset-compute-output.txt @@ -0,0 +1,11 @@ +App initialization finished! + + Asset Compute Worker Tests +Running tests in /home/runner/work/asset-compute-integration-tests/test/asset-compute/worker + ✓ should process image correctly (1234ms) + ✓ should handle errors gracefully (567ms) + ✓ should validate input parameters (123ms) + + 3 passing (1926ms) + +All tests were successful. diff --git a/smoke-tests/tests/fixtures/deploy-extension.txt b/smoke-tests/tests/fixtures/deploy-extension.txt new file mode 100644 index 0000000..cc17ba0 --- /dev/null +++ b/smoke-tests/tests/fixtures/deploy-extension.txt @@ -0,0 +1,32 @@ +Creating project from template @adobe/generator-app-excshell +? Select components to include +✔ Actions: Deploy Runtime actions +✔ Web Assets: Deploy hosted static assets +? Which Adobe I/O App features do you want to enable for this project? +✔ Actions +✔ Events +✔ Web Assets + +App initialization finished! + +Next steps: + $ cd myapp + $ aio app run + +Building actions... +✔ Built 2 action(s) for 'dx-excshell-1' + +Deploying actions... +✔ Deployed 2 action(s) for 'dx-excshell-1' + +Your deployed actions: + -> https://adobeioruntime.net/api/v1/web/dx-excshell-1/generic + +Building web assets... +✔ Built web assets + +Deploying web assets... +✔ Deployed web assets to https://12345-namespace.adobeio-static.net/index.html + +Well done, your app is now online 🏄 +Successful deployment diff --git a/smoke-tests/tests/fixtures/deploy-standalone.txt b/smoke-tests/tests/fixtures/deploy-standalone.txt new file mode 100644 index 0000000..a751ad0 --- /dev/null +++ b/smoke-tests/tests/fixtures/deploy-standalone.txt @@ -0,0 +1,33 @@ +Creating project from template @adobe/generator-app-excshell +? Select components to include +✔ Actions: Deploy Runtime actions +✔ Web Assets: Deploy hosted static assets +? Which Adobe I/O App features do you want to enable for this project? +✔ Actions +✔ Events +✔ Web Assets + +App initialization finished! + +Next steps: + $ cd myapp + $ aio app run + +Building actions... +✔ Built 2 action(s) for 'ffapp' + +Deploying actions... +✔ Deployed 2 action(s) for 'ffapp' + +Your deployed actions: + -> https://adobeioruntime.net/api/v1/web/ffapp/generic + -> https://adobeioruntime.net/api/v1/web/ffapp/publish-events + +Building web assets... +✔ Built web assets + +Deploying web assets... +✔ Deployed web assets to https://12345-namespace.adobeio-static.net/index.html + +Well done, your app is now online 🏄 +Successful deployment diff --git a/smoke-tests/tests/fixtures/init-output.txt b/smoke-tests/tests/fixtures/init-output.txt new file mode 100644 index 0000000..e38d201 --- /dev/null +++ b/smoke-tests/tests/fixtures/init-output.txt @@ -0,0 +1,14 @@ +Creating project from template @adobe/generator-app-excshell +? Select components to include +✔ Actions: Deploy Runtime actions +✔ Web Assets: Deploy hosted static assets +? Which Adobe I/O App features do you want to enable for this project? +✔ Actions +✔ Events +✔ Web Assets + +App initialization finished! + +Next steps: + $ cd myapp + $ aio app run diff --git a/smoke-tests/tests/fixtures/install-output.txt b/smoke-tests/tests/fixtures/install-output.txt new file mode 100644 index 0000000..979feb2 --- /dev/null +++ b/smoke-tests/tests/fixtures/install-output.txt @@ -0,0 +1,4 @@ +Installing app from dist/app.zip... +✔ Extracted app to install-folder +✔ Installed dependencies +Install done. diff --git a/smoke-tests/tests/fixtures/pack-output.txt b/smoke-tests/tests/fixtures/pack-output.txt new file mode 100644 index 0000000..74bdef2 --- /dev/null +++ b/smoke-tests/tests/fixtures/pack-output.txt @@ -0,0 +1,3 @@ +Packaging your app... +✔ Packaged app to dist/app.zip (size: 1.2 MB) +Packaging done. diff --git a/smoke-tests/utils.js b/smoke-tests/utils.js new file mode 100644 index 0000000..b27e499 --- /dev/null +++ b/smoke-tests/utils.js @@ -0,0 +1,202 @@ +#!/usr/bin/env node + +/** + * Utility functions for workflow scripts + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * Run a shell command and return the output + * @param {string} command - The command to run + * @param {object} options - Options for execSync + * @returns {string} - Command output + */ +function runCommand(command, options = {}) { + const defaultOptions = { + encoding: 'utf-8', + stdio: ['inherit', 'pipe', 'pipe'], + ...options + }; + + try { + console.log(`[RUN] ${command}`); + const output = execSync(command, defaultOptions); + return output; + } catch (error) { + console.error(`[ERROR] Command failed: ${command}`); + console.error(`Exit code: ${error.status}`); + console.error(`Output: ${error.stdout}`); + console.error(`Error: ${error.stderr}`); + throw error; + } +} + +/** + * Verify that expected strings exist in a file + * @param {string} filePath - Path to the file to check + * @param {string[]} expectedStrings - Array of strings that must exist in the file + * @throws {Error} If any expected string is not found + */ +function verifyOutput(filePath, expectedStrings) { + if (!fs.existsSync(filePath)) { + throw new Error(`Output file not found: ${filePath}`); + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const missing = []; + + for (const expected of expectedStrings) { + if (!content.includes(expected)) { + missing.push(expected); + } + } + + if (missing.length > 0) { + console.error(`[VERIFICATION FAILED] Missing expected strings in ${filePath}:`); + missing.forEach(str => console.error(` - "${str}"`)); + throw new Error(`Verification failed: ${missing.length} expected string(s) not found`); + } + + console.log(`[VERIFICATION PASSED] All ${expectedStrings.length} expected strings found in ${filePath}`); +} + +/** + * Write environment variables to a .env file + * @param {string} filePath - Path to the .env file + * @param {object} vars - Object with key-value pairs + */ +function writeEnvFile(filePath, vars) { + const lines = Object.entries(vars).map(([key, value]) => `${key}=${value}`); + fs.writeFileSync(filePath, lines.join('\n') + '\n'); + console.log(`[ENV] Created ${filePath} with ${Object.keys(vars).length} variables`); +} + +/** + * Ensure a directory exists + * @param {string} dirPath - Path to the directory + */ +function ensureDir(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`[DIR] Created directory: ${dirPath}`); + } +} + +/** + * Clean a directory (remove if exists, then create) + * @param {string} dirPath - Path to the directory + */ +function cleanDir(dirPath) { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + console.log(`[DIR] Removed directory: ${dirPath}`); + } + ensureDir(dirPath); +} + +/** + * Get runtime credentials from environment variables + * @returns {object|null} Object with AIO_RUNTIME_AUTH and AIO_RUNTIME_NAMESPACE, or null if missing + */ +function getRuntimeCredentials() { + const auth = process.env.AIO_RUNTIME_AUTH; + const namespace = process.env.AIO_RUNTIME_NAMESPACE; + + if (!auth || !namespace) { + return null; + } + + return { + AIO_RUNTIME_AUTH: auth, + AIO_RUNTIME_NAMESPACE: namespace + }; +} + +/** + * Setup runtime environment file + * @param {string} envFilePath - Path to .env file to create + * @returns {boolean} True if credentials were set, false otherwise + */ +function setupRuntimeEnv(envFilePath) { + const creds = getRuntimeCredentials(); + + if (!creds) { + console.log('[SKIP] Runtime credentials not provided (AIO_RUNTIME_AUTH, AIO_RUNTIME_NAMESPACE)'); + console.log('[INFO] Skipping environment setup - this is expected in local testing'); + return false; + } + + writeEnvFile(envFilePath, creds); + return true; +} + +/** + * Create and deploy an app with common logic + * @param {object} options - Configuration options + */ +function createAndDeployApp(options) { + const { + installDeps = false, + cleanFirst = false, + initCommand, + deployCommand = 'app:deploy', + verifyStrings, + successMessage = 'App creation and deployment successful!' + } = options; + + const appDir = 'ffapp'; + const binPath = '../bin/run'; + let stepNum = 1; + + try { + if (installDeps) { + console.log(`=== Step ${stepNum++}: Install dependencies ===`); + runCommand('npm i @adobe/aio-cli-plugin-app-templates'); + runCommand('npm i'); + } + + console.log(`\n=== Step ${stepNum++}: ${cleanFirst ? 'Clean and create' : 'Create'} app directory ===`); + if (cleanFirst) { + cleanDir(appDir); + } else { + ensureDir(appDir); + } + + process.chdir(appDir); + + console.log(`\n=== Step ${stepNum++}: Initialize app ===`); + runCommand(`${binPath} ${initCommand} > consoleoutput.txt`); + verifyOutput('consoleoutput.txt', ['App initialization finished']); + + console.log('\n=== Step 4: Configure environment ==='); + if (!setupRuntimeEnv('.env')) { + return; + } + + console.log(`\n=== Step ${stepNum++}: Deploy app ===`); + runCommand(`${binPath} ${deployCommand} >> consoleoutput.txt`); + + // Verify deployment + verifyOutput('consoleoutput.txt', verifyStrings); + + console.log(`\n${successMessage}`); + + } catch (error) { + console.error('\nScript failed:', error.message); + throw error; + } +} + +module.exports = { + runCommand, + verifyOutput, + writeEnvFile, + ensureDir, + cleanDir, + getRuntimeCredentials, + setupRuntimeEnv, + createAndDeployApp +};