Skip to content

Conversation

@zpratt
Copy link
Owner

@zpratt zpratt commented Nov 2, 2025

  • chore(end-to-end-tests): add a full end to end test that uses real terraform plan output
  • fix: use a pinned version of terraform

Copilot AI review requested due to automatic review settings November 2, 2025 16:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds end-to-end integration testing for Terraform using moto (AWS API mocking) and testcontainers. The changes enable testing of the infra-diff functionality against real Terraform plan outputs in an isolated environment.

  • Implements Terraform integration testing with moto server running in Docker containers
  • Adds development container configuration for consistent local development
  • Configures CI pipeline to run Terraform integration tests with proper version pinning
  • Introduces testcontainers library to manage Docker containers programmatically

Reviewed Changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
package.json Added testcontainers dependency and new npm script for terraform e2e tests
package-lock.json Lock file updates for testcontainers and its transitive dependencies
features/end-to-end-with-terraform.md Feature documentation describing the test goals and requirements
e2e/terraform-integration.test.ts New e2e test file implementing Terraform plan generation and parsing
e2e/terraform/*.tf Terraform fixtures for testing (provider config, backend, SQS resource)
docker-compose.yml Docker compose file for local moto server execution
.terraform-version Terraform version pinning file
.github/workflows/ci.yml Added new CI job for Terraform integration tests
.github/dependabot.yml Added Terraform dependency monitoring
.devcontainer/devcontainer.json Development container configuration

"@vercel/ncc": "^0.38.4",
"@vitest/ui": "^4.0.6",
"chance": "^1.1.13",
"testcontainers": "11.7.2",
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

The testcontainers dependency should use a caret (^) version constraint instead of an exact version pin. This aligns with the project's dependency convention seen in other devDependencies (e.g., 'vitest': '^4.0.6') and allows for automatic patch updates while maintaining compatibility.

Suggested change
"testcontainers": "11.7.2",
"testcontainers": "^11.7.2",

Copilot uses AI. Check for mistakes.
Comment on lines 10 to 52
describe("E2E: Terraform Integration with moto", () => {
const terraformDir = path.join(process.cwd(), "e2e", "terraform");
const planFile = path.join(terraformDir, "plan.bin");
const planJsonFile = path.join(terraformDir, "plan.json");
let motoContainer: StartedTestContainer;
let motoPort: number;

beforeAll(async () => {
// Start moto server using testcontainers
console.log("Starting moto server...");
try {
motoPort = 50000;
motoContainer = await new GenericContainer("motoserver/moto:5.0.0")
.withExposedPorts({ container: 5000, host: motoPort })
.withStartupTimeout(120000)
.start();

console.log(`Moto server is ready on port ${motoPort}`);

// Initialize Terraform
console.log("Initializing Terraform...");
execSync("terraform init", { cwd: terraformDir, stdio: "pipe" });

// Generate plan
console.log("Generating Terraform plan...");
execSync(`terraform plan -out=${planFile}`, {
cwd: terraformDir,
stdio: "pipe",
});

// Convert plan to JSON
console.log("Converting plan to JSON...");
execSync(`terraform show -json ${planFile} > ${planJsonFile}`, {
cwd: terraformDir,
stdio: "pipe",
shell: "/bin/bash",
});
} catch (error) {
console.error("Setup failed:", error);
throw error;
}
}, 120000); // 2 minute timeout for setup

Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Suggested change
describe("E2E: Terraform Integration with moto", () => {
const terraformDir = path.join(process.cwd(), "e2e", "terraform");
const planFile = path.join(terraformDir, "plan.bin");
const planJsonFile = path.join(terraformDir, "plan.json");
let motoContainer: StartedTestContainer;
let motoPort: number;
beforeAll(async () => {
// Start moto server using testcontainers
console.log("Starting moto server...");
try {
motoPort = 50000;
motoContainer = await new GenericContainer("motoserver/moto:5.0.0")
.withExposedPorts({ container: 5000, host: motoPort })
.withStartupTimeout(120000)
.start();
console.log(`Moto server is ready on port ${motoPort}`);
// Initialize Terraform
console.log("Initializing Terraform...");
execSync("terraform init", { cwd: terraformDir, stdio: "pipe" });
// Generate plan
console.log("Generating Terraform plan...");
execSync(`terraform plan -out=${planFile}`, {
cwd: terraformDir,
stdio: "pipe",
});
// Convert plan to JSON
console.log("Converting plan to JSON...");
execSync(`terraform show -json ${planFile} > ${planJsonFile}`, {
cwd: terraformDir,
stdio: "pipe",
shell: "/bin/bash",
});
} catch (error) {
console.error("Setup failed:", error);
throw error;
}
}, 120000); // 2 minute timeout for setup

Copilot uses AI. Check for mistakes.
.withStartupTimeout(120000)
.start();

console.log(`Moto server is ready on port ${motoPort}`);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Suggested change
console.log(`Moto server is ready on port ${motoPort}`);
console.log(JSON.stringify({ event: "moto_server_ready", port: motoPort }));

Copilot uses AI. Check for mistakes.
console.log(`Moto server is ready on port ${motoPort}`);

// Initialize Terraform
console.log("Initializing Terraform...");
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Suggested change
console.log("Initializing Terraform...");
console.log(JSON.stringify({ message: "Initializing Terraform..." }));

Copilot uses AI. Check for mistakes.
execSync("terraform init", { cwd: terraformDir, stdio: "pipe" });

// Generate plan
console.log("Generating Terraform plan...");
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Suggested change
console.log("Generating Terraform plan...");
console.log(JSON.stringify({ event: "Generating Terraform plan" }));

Copilot uses AI. Check for mistakes.
unlinkSync(lockFile);
}
} catch (error) {
console.error("Failed to cleanup lock file:", error);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Copilot uses AI. Check for mistakes.
try {
if (motoContainer) {
await motoContainer.stop();
console.log("Moto container stopped");
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.log for test output violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Copilot uses AI. Check for mistakes.
console.log("Moto container stopped");
}
} catch (error) {
console.error("Failed to stop moto container:", error);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

Using console.error violates the operational convention stated in the coding guidelines: 'ensure all application logs are structured as JSON objects, except when logging to the console in GitHub Actions.' Since this is a test file, consider using the test framework's logging capabilities or a structured logger.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 51
beforeAll(async () => {
// Start moto server using testcontainers
console.log("Starting moto server...");
try {
motoPort = 50000;
motoContainer = await new GenericContainer("motoserver/moto:5.0.0")
.withExposedPorts({ container: 5000, host: motoPort })
.withStartupTimeout(120000)
.start();

console.log(`Moto server is ready on port ${motoPort}`);

// Initialize Terraform
console.log("Initializing Terraform...");
execSync("terraform init", { cwd: terraformDir, stdio: "pipe" });

// Generate plan
console.log("Generating Terraform plan...");
execSync(`terraform plan -out=${planFile}`, {
cwd: terraformDir,
stdio: "pipe",
});

// Convert plan to JSON
console.log("Converting plan to JSON...");
execSync(`terraform show -json ${planFile} > ${planJsonFile}`, {
cwd: terraformDir,
stdio: "pipe",
shell: "/bin/bash",
});
} catch (error) {
console.error("Setup failed:", error);
throw error;
}
}, 120000); // 2 minute timeout for setup
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

The beforeAll hook contains multiple complex setup steps (container startup, terraform init, plan, and JSON conversion) that violate the principle of single responsibility. Consider extracting these steps into separate, well-named helper functions to improve readability and maintainability.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,15 @@
---
version: '3.8'
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

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

The 'version' field in docker-compose.yml is deprecated as of Docker Compose v1.27.0+ and is no longer required. Consider removing this line as modern versions of Docker Compose ignore it.

Suggested change
version: '3.8'

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings November 16, 2025 23:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

node-version-file: ".nvmrc"
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_version: 1.13.4
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

The Terraform version is hardcoded as "1.13.4" here, but it should be read from the .terraform-version file to maintain a single source of truth. Consider using terraform_version_file: ".terraform-version" instead of terraform_version: 1.13.4 to avoid version drift.

Suggested change
terraform_version: 1.13.4
terraform_version_file: ".terraform-version"

Copilot uses AI. Check for mistakes.
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers-contrib/features/terraform-asdf:2": {
"version": "1.13.4"
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

The Terraform version is duplicated here. It should be read from the .terraform-version file to maintain consistency. The devcontainer feature supports using a version file reference instead of hardcoding the version.

Suggested change
"version": "1.13.4"
"version": "file:.terraform-version"

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
terraformContainer = await new GenericContainer(
`hashicorp/terraform:${terraformVersion}`,
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

The version string used in this template literal comes from reading the .terraform-version file, but there's no validation that the version format is correct or that the Docker image tag exists. Consider adding validation or error handling if the version is malformed or the image doesn't exist with that tag.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +210
afterAll(async () => {
// Cleanup
console.log("Cleaning up...");

// Have terraform container clean up its own files with proper permissions
try {
if (terraformContainer) {
console.log("Cleaning up terraform files from container...");
await terraformContainer.exec([
"sh",
"-c",
"rm -rf /terraform/.terraform /terraform/plan.bin /terraform/plan.json /terraform/.terraform.lock.hcl",
]);
console.log("Terraform files cleaned from container");
}
} catch (error) {
console.warn("Failed to cleanup from container:", error);
}

// Stop and remove terraform container
try {
if (terraformContainer) {
await terraformContainer.stop();
console.log("Terraform container stopped");
}
} catch (error) {
console.error("Failed to stop terraform container:", error);
}

// Stop and remove moto container
try {
if (motoContainer) {
await motoContainer.stop();
console.log("Moto container stopped");
}
} catch (error) {
console.error("Failed to stop moto container:", error);
}

// Stop and remove network
try {
if (network) {
await network.stop();
console.log("Network stopped");
}
} catch (error) {
console.error("Failed to stop network:", error);
}

// Remove plan files from host
try {
if (existsSync(planFile)) {
unlinkSync(planFile);
}
if (existsSync(planJsonFile)) {
unlinkSync(planJsonFile);
}
} catch (error) {
console.error("Failed to cleanup plan files:", error);
}

// Remove .terraform directory
try {
const terraformStateDir = path.join(terraformDir, ".terraform");
if (existsSync(terraformStateDir)) {
rmSync(terraformStateDir, { recursive: true, force: true });
console.log(".terraform directory cleaned up");
}
} catch (error) {
// Log error but don't fail - permissions issues in CI can be expected
console.warn("Failed to cleanup .terraform directory:", error);
}

// Remove lock file
try {
const lockFile = path.join(terraformDir, ".terraform.lock.hcl");
if (existsSync(lockFile)) {
unlinkSync(lockFile);
}
} catch (error) {
console.error("Failed to cleanup lock file:", error);
}
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The cleanup logic in the afterAll hook has multiple try-catch blocks that swallow errors. While some failures during cleanup are acceptable (as noted in comments), the current approach makes it difficult to distinguish between expected and unexpected failures. Consider consolidating cleanup logic and using more specific error handling to ensure critical cleanup steps don't fail silently.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,16 @@
# End to End with Terraform

Goal: Demonstrate how Infra Diff can be used in an end-to-end workflow with Terraform and moto to provide a fake of the AWS API. The goal is to produce a real binary terraform plan, convert it to json, and then run infra-diff against that json plan.
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

The feature description mentions using "moto to provide a fake of the AWS API" and creating infrastructure on AWS, but then describes using moto to "mock" the AWS API. The terminology is inconsistent. Consider using consistent terminology (either "mock" or "fake") throughout the document to avoid confusion.

Suggested change
Goal: Demonstrate how Infra Diff can be used in an end-to-end workflow with Terraform and moto to provide a fake of the AWS API. The goal is to produce a real binary terraform plan, convert it to json, and then run infra-diff against that json plan.
Goal: Demonstrate how Infra Diff can be used in an end-to-end workflow with Terraform and moto to mock the AWS API. The goal is to produce a real binary terraform plan, convert it to json, and then run infra-diff against that json plan.

Copilot uses AI. Check for mistakes.
## Requirements

- Ensure the pipeline uses setup-terraform action to install terraform
- Ensure we're testing with the latest version of terraform
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

The requirement states "Ensure we're testing with the latest version of terraform," but version 1.13.4 is specified. This creates ambiguity - should the version be pinned (as is done) or should it actually use the latest version? Consider clarifying this requirement to reflect the actual implementation (using a pinned version for consistency) rather than "latest."

Suggested change
- Ensure we're testing with the latest version of terraform
- Ensure we're testing with a pinned version of terraform (currently 1.13.4) for consistency and reproducibility

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants