diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index dad8437613..612375ca1c 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -41,31 +41,27 @@ jobs: - name: yarn install run: yarn install --frozen-lockfile - run: yarn build - # generate temp directory - - name: generate tempdir - id: generate_tempdir - run: echo "::set-output name=tempdir::$(node scripts/generateTempDir.js)" - # This creates a new app and plugin which pollutes the workspace, so it should be run last. - name: verify app and plugin creation on Windows - working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} + working-directory: ${{ runner.temp }} if: runner.os == 'Windows' - run: node ${{ github.workspace }}/scripts/cli-e2e-test.js + run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js env: BACKSTAGE_E2E_CLI_TEST: true - name: verify app and plugin creation on Linux - working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} + working-directory: ${{ runner.temp }} if: runner.os == 'Linux' run: | sudo sysctl fs.inotify.max_user_watches=524288 - node ${{ github.workspace }}/scripts/cli-e2e-test.js + node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js env: BACKSTAGE_E2E_CLI_TEST: true - # This should lint and test both an app and a plugin - - name: yarn lint, test after creation - working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} - run: | - cd test-app - yarn lint:all - yarn test:all + - name: lint newly created app and plugin + run: yarn lint:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: test newly created app and plugin + run: yarn test:all + working-directory: ${{ runner.temp }}/test-app env: BACKSTAGE_E2E_CLI_TEST: true diff --git a/package.json b/package.json index 10a5daa4c1..f04c982621 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "lerna": "^3.20.2", "lint-staged": "^10.1.0", "prettier": "^1.19.1", - "typescript": "^3.7.5", - "zombie": "^6.1.4" + "typescript": "^3.7.5" }, "dependencies": { "@types/classnames": "^2.2.9", diff --git a/scripts/generateTempDir.js b/packages/cli/e2e-test/.eslintrc.js similarity index 62% rename from scripts/generateTempDir.js rename to packages/cli/e2e-test/.eslintrc.js index e455a29320..274c7426b8 100644 --- a/scripts/generateTempDir.js +++ b/packages/cli/e2e-test/.eslintrc.js @@ -14,17 +14,16 @@ * limitations under the License. */ -const { handleError } = require('./helpers'); - -async function generateTempDir() { - const tempDir = await require('fs-extra').mkdtemp( - require('path').join(require('os').tmpdir(), 'backstage-e2e-'), - ); - process.stdout.write(tempDir); - return tempDir; -} - -module.exports = generateTempDir; - -process.on('unhandledRejection', handleError); -generateTempDir().catch(handleError); +module.exports = { + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + }, +}; diff --git a/scripts/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js similarity index 73% rename from scripts/cli-e2e-test.js rename to packages/cli/e2e-test/cli-e2e-test.js index 4af26e2e41..c8374af0eb 100644 --- a/scripts/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -14,6 +14,8 @@ * limitations under the License. */ +const os = require('os'); +const fs = require('fs-extra'); const { resolve: resolvePath } = require('path'); const Browser = require('zombie'); @@ -27,35 +29,25 @@ const { const createTestApp = require('./createTestApp'); const createTestPlugin = require('./createTestPlugin'); -const generateTempDir = require('./generateTempDir.js'); Browser.localhost('localhost', 3000); +async function createTempDir() { + return fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); +} + async function main() { process.env.BACKSTAGE_E2E_CLI_TEST = 'true'; - const rootDir = process.env.CI - ? resolvePath(process.env.GITHUB_WORKSPACE) - : resolvePath(__dirname, '..'); - - const tempDir = process.env.CI ? process.cwd() : await generateTempDir(); + const workDir = process.env.CI ? process.cwd() : await createTempDir(); process.stdout.write(`Initial directory: ${process.cwd()}\n`); - process.chdir(tempDir); - process.stdout.write(`Temp directory: ${process.cwd()}\n`); + process.chdir(workDir); + process.stdout.write(`Working directory: ${process.cwd()}\n`); - await waitForExit(spawnPiped(['yarn', 'init --yes'])); + await createTestApp(); - const createCmdPath = require('path').join( - rootDir, - 'packages', - 'cli', - 'bin', - 'backstage-cli', - ); - await createTestApp(`${createCmdPath} create-app`); - - const appDir = resolvePath(tempDir, 'test-app'); + const appDir = resolvePath(workDir, 'test-app'); process.chdir(appDir); process.stdout.write(`App directory: ${appDir}\n`); diff --git a/scripts/createTestApp.js b/packages/cli/e2e-test/createTestApp.js similarity index 84% rename from scripts/createTestApp.js rename to packages/cli/e2e-test/createTestApp.js index 80f7bf3b18..5437804975 100644 --- a/scripts/createTestApp.js +++ b/packages/cli/e2e-test/createTestApp.js @@ -14,11 +14,14 @@ * limitations under the License. */ +const { resolve: resolvePath } = require('path'); const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); -async function createTestApp(cmd) { +async function createTestApp() { + const cliPath = resolvePath(__dirname, '../bin/backstage-cli'); + print('Creating a Backstage App'); - const createApp = spawnPiped(['node', cmd]); + const createApp = spawnPiped(['node', cliPath, 'create-app']); try { let stdout = ''; diff --git a/scripts/createTestPlugin.js b/packages/cli/e2e-test/createTestPlugin.js similarity index 100% rename from scripts/createTestPlugin.js rename to packages/cli/e2e-test/createTestPlugin.js diff --git a/scripts/helpers.js b/packages/cli/e2e-test/helpers.js similarity index 100% rename from scripts/helpers.js rename to packages/cli/e2e-test/helpers.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 2a2adba6a8..faf8710d14 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,6 +22,7 @@ "build": "backstage-cli build-cache -- tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", + "test:e2e": "node e2e-test/cli-e2e-test.js", "clean": "backstage-cli clean", "start": "nodemon ." }, @@ -41,7 +42,8 @@ "del": "^5.1.0", "nodemon": "^2.0.2", "ts-node": "^8.6.2", - "tsconfig-paths": "^3.9.0" + "tsconfig-paths": "^3.9.0", + "zombie": "^6.1.4" }, "bin": { "backstage-cli": "bin/backstage-cli" diff --git a/packages/cli/src/commands/build-cache/index.ts b/packages/cli/src/commands/build-cache/index.ts index a825a7f415..a8a1a27e9d 100644 --- a/packages/cli/src/commands/build-cache/index.ts +++ b/packages/cli/src/commands/build-cache/index.ts @@ -30,7 +30,7 @@ export async function withCache( buildFunc: () => Promise, ): Promise { const key = await Cache.readInputKey(options.inputs); - if (!key || process.env.BACKSTAGE_E2E_CLI_TEST) { + if (!key) { print('input directory is dirty, skipping cache'); await fs.remove(options.output); await buildFunc(); diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index 219406704d..cb34dc0686 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -86,26 +86,22 @@ export async function moveApp( }); } -async function addPackageResolutions(rootDir: string, appDir: string) { - process.chdir(appDir); - - const packageFileContent = await fs.readFile('package.json', 'utf-8'); +async function addPackageResolutions(appDir: string) { + const pkgJsonPath = resolvePath(appDir, 'package.json'); + const packageFileContent = await fs.readFile(pkgJsonPath, 'utf-8'); const packageFileJson = JSON.parse(packageFileContent); - if (packageFileJson.resolutions) { - throw new Error('package.json already contains resolutions'); - } - packageFileJson.resolutions = {}; + packageFileJson.resolutions = packageFileJson.resolutions || {}; const packages = ['cli', 'core', 'test-utils', 'test-utils-core', 'theme']; for (const pkg of packages) { await Task.forItem('adding', `${pkg} link to package.json`, async () => { - const pkgPath = require('path').join(rootDir, 'packages', pkg); + const pkgPath = paths.resolveOwnRoot('packages', pkg); packageFileJson.resolutions[`@backstage/${pkg}`] = `file:${pkgPath}`; const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; - await fs.writeFile('package.json', newContents, 'utf-8').catch(error => { + await fs.writeFile(pkgJsonPath, newContents, 'utf-8').catch(error => { throw new Error( `Failed to add resolutions to package.json: ${error.message}`, ); @@ -157,10 +153,7 @@ export default async () => { // e2e testing needs special treatment if (process.env.BACKSTAGE_E2E_CLI_TEST) { Task.section('Linking packages locally for e2e tests'); - const rootDir = process.env.CI - ? resolvePath(process.env.GITHUB_WORKSPACE!) - : resolvePath(__dirname, '..', '..', '..'); - await addPackageResolutions(rootDir, appDir); + await addPackageResolutions(appDir); } Task.section('Building the app'); diff --git a/packages/cli/src/helpers/paths.ts b/packages/cli/src/helpers/paths.ts index 173e77e1c5..e7b351ec16 100644 --- a/packages/cli/src/helpers/paths.ts +++ b/packages/cli/src/helpers/paths.ts @@ -25,6 +25,9 @@ export type Paths = { // Root dir of the cli itself, containing package.json ownDir: string; + // Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo. + ownRoot: string; + // The location of the app that the cli is being executed in targetDir: string; @@ -34,6 +37,9 @@ export type Paths = { // Resolve a path relative to own repo resolveOwn: ResolveFunc; + // Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo. + resolveOwnRoot: ResolveFunc; + // Resolve a path relative to the app resolveTarget: ResolveFunc; @@ -91,10 +97,31 @@ export function findOwnDir() { return resolvePath(__dirname, path); } +// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo. +export function findOwnRootPath(ownDir: string) { + const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src')); + if (!isLocal) { + throw new Error( + 'Tried to access monorepo package root dir outside of Backstage repository', + ); + } + + return resolvePath(ownDir, '../..'); +} + export function findPaths(): Paths { const ownDir = findOwnDir(); const targetDir = fs.realpathSync(process.cwd()); + // Lazy load this as it will throw an error if we're not inside the Backstage repo. + let ownRoot = ''; + const getOwnRoot = () => { + if (!ownRoot) { + ownRoot = findOwnRootPath(ownDir); + } + return ownRoot; + }; + // We're not always running in a monorepo, so we lazy init this to only crash commands // that require a monorepo when we're not in one. let targetRoot = ''; @@ -107,11 +134,15 @@ export function findPaths(): Paths { return { ownDir, + get ownRoot() { + return getOwnRoot(); + }, targetDir, get targetRoot() { return getTargetRoot(); }, resolveOwn: (...paths) => resolvePath(ownDir, ...paths), + resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths), resolveTarget: (...paths) => resolvePath(targetDir, ...paths), resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), };