Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .codex/environments/environment.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for scripts or documentation that generate environment.toml

# Search for references to generating or creating this environment file
rg -i "environment\.toml|generate.*environment" -g '!.codex/environments/*' -C3

# Look for any .codex documentation
fd -e md -e txt . .codex/

Repository: toolsplus/nx-forge

Length of output: 44


🏁 Script executed:

# Explore .codex directory structure
find .codex -type f -o -type d | head -20

# Search for any setup, build, or generation scripts
fd -e sh -e js -e py -e ts -e bash | xargs rg -l "environment|generate" | head -20

# Check .codex/config.toml if it exists
if [ -f ".codex/config.toml" ]; then
  echo "=== .codex/config.toml ===";
  cat .codex/config.toml;
fi

# Look for any scripts in the repo root or .codex
ls -la .codex/*.* 2>/dev/null || true
ls -la *.js *.json *.sh 2>/dev/null || true

Repository: toolsplus/nx-forge

Length of output: 1837


🏁 Script executed:

# View the environment.toml file content
echo "=== .codex/environments/environment.toml ===" 
cat .codex/environments/environment.toml

# Search for any references to this environment or "nx-forge"
echo -e "\n=== References to nx-forge environment ==="
rg -i "nx-forge|environments" --type toml

# Search for environment setup/configuration in docs or comments
echo -e "\n=== Environment-related documentation ==="
fd -e md -e txt | xargs rg -l -i "environment|codex" 2>/dev/null | head -10

Repository: toolsplus/nx-forge

Length of output: 533


🏁 Script executed:

# Check the documentation files mentioned
echo "=== docs/guides/getting-started.md (first 100 lines) ==="
head -100 docs/guides/getting-started.md

echo -e "\n=== docs/guides/transforming-the-manifest.md (first 100 lines) ==="
head -100 docs/guides/transforming-the-manifest.md

# Search for any Codex environment patterns or examples
echo -e "\n=== References to environment setup ==="
rg -i "environment|setup.*script" docs/ | head -20

Repository: toolsplus/nx-forge

Length of output: 10690


Remove the autogenerated comment—this file is manually maintained.

The comment on line 1 claims the file is autogenerated, but no generation script or tool was found in the codebase. Since this environment is being manually created and maintained in the PR, the misleading comment should be removed to avoid confusing future maintainers.

Additionally, this environment is defined but not referenced in .codex/config.toml, so it won't be activated. Either activate it in the main config or document why this environment definition is needed if it's meant to be optional.

Consider using npm ci instead of npm i in the setup script for reproducible, deterministic builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.codex/environments/environment.toml at line 1, Remove the misleading
autogenerated comment at the top of .codex/environments/environment.toml (delete
the line "# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY"), then either add this
environment to .codex/config.toml so it will be activated (reference the
environment identifier used inside environment.toml) or add a short comment in
environment.toml explaining why it's intentionally optional; finally, in the
environment's setup script replace any occurrence of "npm i" with "npm ci" to
ensure reproducible installs.

version = 1
name = "nx-forge"

[setup]
script = "npm i"
5 changes: 2 additions & 3 deletions e2e/nx-forge-e2e/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
"e2e": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "e2e/nx-forge-e2e/jest.config.js",
"runInBand": true
"jestConfig": "e2e/nx-forge-e2e/jest.config.js"
},
"dependsOn": ["nx-forge:build"]
"dependsOn": ["^build"]
}
},
"tags": [],
Expand Down
78 changes: 78 additions & 0 deletions e2e/nx-forge-e2e/src/application.generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { generateForgeApp } from './utils/generate-forge-app';
import { runNxCommandAsync } from './utils/async-commands';
import { cleanupTestProject, createTestProject } from './utils/test-project';

describe('Forge application generator', () => {
let projectDirectory: string;

beforeAll(() => {
projectDirectory = createTestProject();
});

afterAll(async () => {
try {
if (projectDirectory) {
await runNxCommandAsync('reset', { cwd: projectDirectory });
}
} finally {
cleanupTestProject(projectDirectory);
}
});

it('should generate a Forge app', async () => {
const appName = await generateForgeApp({
cwd: projectDirectory,
directory: 'apps',
options: '--bundler=webpack',
});
expect(
existsSync(join(projectDirectory, 'apps', appName, 'manifest.yml'))
).toBe(true);
expect(
existsSync(join(projectDirectory, 'apps', appName, 'webpack.config.js'))
).toBe(true);
expect(
existsSync(join(projectDirectory, 'apps', appName, 'src', 'index.ts'))
).toBe(true);
});

describe('--directory', () => {
it('should generate a Forge app in the specified directory', async () => {
const subdir = 'subdir';
const appName = await generateForgeApp({
cwd: projectDirectory,
directory: subdir,
options: `--bundler=webpack`,
});

expect(
existsSync(join(projectDirectory, subdir, appName, 'manifest.yml'))
).toBe(true);
expect(
existsSync(join(projectDirectory, subdir, appName, 'webpack.config.js'))
).toBe(true);
expect(
existsSync(join(projectDirectory, subdir, appName, 'src', 'index.ts'))
).toBe(true);
});
});

describe('--tags', () => {
it('should generate a Forge app with tags added to the project', async () => {
const appName = await generateForgeApp({
cwd: projectDirectory,
directory: 'apps',
options: `--tags e2etag,e2ePackage`,
});
const project = JSON.parse(
readFileSync(
join(projectDirectory, 'apps', appName, 'project.json'),
'utf8'
)
);
expect(project.tags).toEqual(['e2etag', 'e2ePackage']);
});
});
});
183 changes: 183 additions & 0 deletions e2e/nx-forge-e2e/src/basic-setup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { GraphQLClient } from 'graphql-request';
import { generateForgeApp } from './utils/generate-forge-app';
import {
Credentials,
ForgeInstallationContext,
getCredentials,
getDeveloperSpaceId,
getForgeInstallationContext,
} from './utils/config';
import { createClient, deleteApp } from './utils/atlassian-graphql-client';
import {
runCommandAsync,
runForgeCommandAsync,
runNxCommandAsync,
} from './utils/async-commands';
import {
cleanupTestProject,
createTestProject,
} from './utils/test-project';
import stripAnsi = require('strip-ansi');

describe('Forge lifecycle', () => {
// initialize before all tests
let projectDirectory: string;
let developerCredentials: Credentials;
let apiClient: GraphQLClient;
let installationContext: ForgeInstallationContext;
let developerSpaceId: string;

beforeAll(async () => {
projectDirectory = createTestProject();
developerCredentials = getCredentials();
apiClient = createClient(developerCredentials);
installationContext = getForgeInstallationContext();
developerSpaceId = getDeveloperSpaceId();

// Initialize the Forge CLI, otherwise commands may fail due to expected interactive input
await runCommandAsync(`npx forge settings set usage-analytics false`, {
cwd: projectDirectory,
silenceError: true,
});
});

afterAll(async () => {
try {
if (projectDirectory) {
await runNxCommandAsync('reset', { cwd: projectDirectory });
}
} finally {
cleanupTestProject(projectDirectory);
}
});

it('should generate, build, package, register, deploy and install a Forge app', async () => {
const appName = await generateForgeApp({
cwd: projectDirectory,
directory: 'apps',
});

// Build

const nxBuildResult = await runNxCommandAsync(`run ${appName}:build`, {
cwd: projectDirectory,
silenceError: true,
});
expect(nxBuildResult.stderr).toEqual('');
expect(stripAnsi(nxBuildResult.stdout)).toContain(
'Successfully ran target build for project'
);

// Package

const nxPackageResult = await runNxCommandAsync(`run ${appName}:package`, {
cwd: projectDirectory,
});
expect(nxPackageResult.stderr).toEqual('');
expect(stripAnsi(nxPackageResult.stdout)).toEqual(
expect.stringContaining('Successfully ran target package for project')
);

// Register

const unregisteredOutputManifestContent = readFileSync(
join(projectDirectory, 'dist', 'apps', appName, 'manifest.yml'),
'utf8'
);
expect(unregisteredOutputManifestContent).toContain(
'ari:cloud:ecosystem::app/to-be-generated'
);

const nxRegisterResult = await runNxCommandAsync(
`run ${appName}:register --accept-terms --developer-space-id ${developerSpaceId}`,
{
cwd: projectDirectory,
silenceError: true,
}
);
expect(nxRegisterResult.stderr).toEqual('');
expect(stripAnsi(nxRegisterResult.stdout)).toContain(
'Forge app registered'
);

// ari:cloud:ecosystem::app/<uuid>
const registeredAppIdRegex =
/ari:cloud:ecosystem::app\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;

const registeredOutputManifestContent = readFileSync(
join(projectDirectory, 'dist', 'apps', appName, 'manifest.yml'),
'utf8'
);
const [registeredAppId] =
registeredOutputManifestContent.match(registeredAppIdRegex) ?? [];
expect(registeredAppId).not.toBeNull();
expect(registeredAppId).toBeDefined();
expect(registeredAppId).not.toEqual('');

const projectManifestContent = readFileSync(
join(projectDirectory, 'apps', appName, 'manifest.yml'),
'utf8'
);
expect(projectManifestContent).toContain(registeredAppId);

try {
// Deploy

// Run with `--no-verfiy` because the generated blank app template causes linting errors
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in comment: "verfiy" should be "verify".

✏️ Fix typo
-      // Run with `--no-verfiy` because the generated blank app template causes linting errors
+      // Run with `--no-verify` because the generated blank app template causes linting errors
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Run with `--no-verfiy` because the generated blank app template causes linting errors
// Run with `--no-verify` because the generated blank app template causes linting errors
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/nx-forge-e2e/src/basic-setup.spec.ts` at line 128, Fix the typo in the
inline comment that reads "// Run with `--no-verfiy` because the generated blank
app template causes linting errors" by changing "verfiy" to "verify" so the
comment reads "// Run with `--no-verify` because the generated blank app
template causes linting errors"; update the comment where it appears in
basic-setup.spec.ts (the test comment near the e2e setup block) to ensure
consistency.

const nxDeployResult = await runNxCommandAsync(
`run ${appName}:deploy --no-verify`,
{
cwd: projectDirectory,
silenceError: true,
}
);
expect(nxDeployResult.stderr).toEqual('');
expect(stripAnsi(nxDeployResult.stdout)).toContain('Forge app deployed');

// Install using Forge CLI

const installResult = await runForgeCommandAsync(
`install --product=${installationContext.product} --site=${installationContext.siteUrl} --environment ${installationContext.environment} --non-interactive`,
{
cwd: join(projectDirectory, 'dist', 'apps', appName),
silenceError: true,
}
);
expect(installResult.stderr).toEqual('');
expect(stripAnsi(installResult.stdout)).toMatch(/Install.*complete/);
} finally {
if (registeredAppId) {
try {
await runForgeCommandAsync(
`uninstall --product=${installationContext.product} --site=${installationContext.siteUrl} --environment ${installationContext.environment} --non-interactive`,
{
cwd: join(projectDirectory, 'dist', 'apps', appName),
silenceError: true,
}
);
} catch (error) {
console.warn(
`Failed to uninstall Forge app ${registeredAppId}`,
error
);
}

try {
const result = await deleteApp(registeredAppId)(apiClient);
if (!result.success) {
console.warn(
`Failed to delete registered app ${registeredAppId}: ${result.errors}`
);
}
Comment on lines +169 to +173
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error array won't format properly in the warning message.

result.errors is an array of objects [{ message: string }]. Using it directly in a template literal will output [object Object] instead of useful information.

✏️ Fix error formatting
           if (!result.success) {
             console.warn(
-              `Failed to delete registered app ${registeredAppId}: ${result.errors}`
+              `Failed to delete registered app ${registeredAppId}: ${result.errors?.map(e => e.message).join(', ')}`
             );
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!result.success) {
console.warn(
`Failed to delete registered app ${registeredAppId}: ${result.errors}`
);
}
if (!result.success) {
console.warn(
`Failed to delete registered app ${registeredAppId}: ${result.errors?.map(e => e.message).join(', ')}`
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/nx-forge-e2e/src/basic-setup.spec.ts` around lines 169 - 173, The warning
uses result.errors (an array of objects) directly which prints as [object
Object]; update the log to format the errors, e.g. map the array to messages or
stringify it before interpolating so the warning shows useful details. Locate
the block referencing result and registeredAppId in basic-setup.spec.ts (the if
(!result.success) branch) and replace the template interpolation with a
formatted string such as result.errors.map(e => e.message).join(', ') or
JSON.stringify(result.errors) when constructing the console.warn message.

} catch (error) {
console.warn(
`Failed to delete registered app ${registeredAppId}`,
error
);
}
}
}
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { exec } from 'child_process';
import {
detectPackageManager,
getPackageManagerCommand,
} from '@nx/devkit';
import { tmpProjPath } from '@nx/plugin/testing';
import { detectPackageManager, getPackageManagerCommand } from '@nx/devkit';

const getCommandEnv = (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {
const commandEnv = { ...process.env, ...env };
Expand All @@ -17,7 +13,7 @@ const getCommandEnv = (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {
};

/**
* Runs the given command asynchronously inside the e2e directory (if cwd is not provided).
* Runs the given command asynchronously inside the provided working directory.
*
* This is a local re-implementation of the helper from `@nx/plugin/testing`
* so the e2e suite can control the child process environment.
Expand All @@ -32,15 +28,13 @@ const getCommandEnv = (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {
*/
export const runCommandAsync = (
command: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
opts: { cwd: string; silenceError?: boolean; env?: NodeJS.ProcessEnv }
): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
exec(
command,
{
cwd: opts.cwd ?? tmpProjPath(),
cwd: opts.cwd,
env: getCommandEnv(opts.env),
windowsHide: true,
},
Expand All @@ -55,7 +49,7 @@ export const runCommandAsync = (
};

/**
* Runs an Nx command asynchronously inside the e2e directory.
* Runs an Nx command asynchronously inside the provided working directory.
*
* This mirrors `runNxCommandAsync` from `@nx/plugin/testing`, but delegates to
* the local `runCommandAsync` above so the same NO_COLOR cleanup is applied to
Expand All @@ -65,21 +59,17 @@ export const runCommandAsync = (
*/
export const runNxCommandAsync = (
command: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
opts: { cwd: string; silenceError?: boolean; env?: NodeJS.ProcessEnv }
): Promise<{ stdout: string; stderr: string }> => {
const cwd = opts.cwd ?? tmpProjPath();
const pmc = getPackageManagerCommand(detectPackageManager(cwd));
const pmc = getPackageManagerCommand(detectPackageManager(opts.cwd));

return runCommandAsync(`${pmc.exec} nx ${command}`, {
...opts,
cwd,
});
};

/**
* Runs the given Forge CLI command asynchronously inside the e2e directory (if cwd is not provided).
* Runs the given Forge CLI command asynchronously inside the provided working directory.
*
* Note that this implementation is only meant to be used in testing code. It is using `exec`
* to run the Forge CLI command. `exec` returns `stdout` and `stderr` as strings which is convenient
Expand All @@ -93,16 +83,14 @@ export const runNxCommandAsync = (
*/
export const runForgeCommandAsync = (
command: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
opts: { cwd: string; silenceError?: boolean; env?: NodeJS.ProcessEnv }
): Promise<{ stdout: string; stderr: string }> => {
const pmc = getPackageManagerCommand();
return new Promise((resolve, reject) => {
exec(
`${pmc.exec} forge ${command}`,
{
cwd: opts.cwd ?? tmpProjPath(),
cwd: opts.cwd,
env: getCommandEnv(opts.env),
},
(err, stdout, stderr) => {
Expand Down
Loading
Loading