diff --git a/biome.json b/biome.json index 6b5a0ad..79b3950 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ "defaultBranch": "main" }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["**/*.ts", "**/*.js", "**/*.json", "!fixtures"] }, "formatter": { "enabled": true, diff --git a/e2e/json-parsing.test.ts b/e2e/json-parsing.test.ts new file mode 100644 index 0000000..10fa6bc --- /dev/null +++ b/e2e/json-parsing.test.ts @@ -0,0 +1,175 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; +import { ParsePlanUseCase } from "../src/domain/usecases/ParsePlanUseCase"; +import { TextFormatterUseCase } from "../src/domain/usecases/TextFormatterUseCase"; + +describe("E2E: JSON Parsing", () => { + describe("Plan parsing", () => { + it("should parse a plan with no changes", async () => { + const content = await readFile("./fixtures/sample-plan.json", "utf-8"); + const parser = new ParsePlanUseCase(); + + const plan = await parser.parse(content); + + expect(plan.resourceChanges).toHaveLength(0); + }); + + it("should parse a plan with additions", async () => { + const content = await readFile( + "./fixtures/plan-with-changes.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + + const plan = await parser.parse(content); + + expect(plan.resourceChanges).toHaveLength(1); + expect(plan.resourceChanges[0].actions).toContain("create"); + }); + + it("should parse a plan with updates", async () => { + const content = await readFile( + "./fixtures/plan-with-updates.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + + const plan = await parser.parse(content); + + expect(plan.resourceChanges).toHaveLength(1); + expect(plan.resourceChanges[0].actions).toContain("update"); + }); + + it("should parse a plan with deletions", async () => { + const content = await readFile( + "./fixtures/plan-with-deletions.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + + const plan = await parser.parse(content); + + expect(plan.resourceChanges).toHaveLength(1); + expect(plan.resourceChanges[0].actions).toContain("delete"); + }); + + it("should parse a plan with mixed changes", async () => { + const content = await readFile( + "./fixtures/plan-with-mixed-changes.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + + const plan = await parser.parse(content); + + expect(plan.resourceChanges).toHaveLength(3); + + const creates = plan.resourceChanges.filter((rc) => + rc.actions.includes("create"), + ); + const updates = plan.resourceChanges.filter((rc) => + rc.actions.includes("update"), + ); + const deletes = plan.resourceChanges.filter((rc) => + rc.actions.includes("delete"), + ); + + expect(creates).toHaveLength(1); + expect(updates).toHaveLength(1); + expect(deletes).toHaveLength(1); + }); + + it("should reject malformed JSON with descriptive error", async () => { + const content = await readFile("./fixtures/malformed-plan.json", "utf-8"); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(content)).rejects.toThrow( + "Invalid JSON in plan file", + ); + }); + }); + + describe("Text formatting", () => { + it("should format a plan with no changes", async () => { + const content = await readFile("./fixtures/sample-plan.json", "utf-8"); + const parser = new ParsePlanUseCase(); + const formatter = new TextFormatterUseCase(); + + const plan = await parser.parse(content); + const result = formatter.format(plan); + + expect(result).toContain("No changes"); + expect(result).toContain("0 to add"); + expect(result).toContain("0 to change"); + expect(result).toContain("0 to destroy"); + }); + + it("should format a plan with additions", async () => { + const content = await readFile( + "./fixtures/plan-with-changes.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + const formatter = new TextFormatterUseCase(); + + const plan = await parser.parse(content); + const result = formatter.format(plan); + + expect(result).toContain("1 to add"); + expect(result).toContain("aws_s3_bucket.example"); + expect(result).toContain("create"); + }); + + it("should format a plan with updates", async () => { + const content = await readFile( + "./fixtures/plan-with-updates.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + const formatter = new TextFormatterUseCase(); + + const plan = await parser.parse(content); + const result = formatter.format(plan); + + expect(result).toContain("1 to change"); + expect(result).toContain("aws_s3_bucket.updated"); + expect(result).toContain("update"); + }); + + it("should format a plan with deletions", async () => { + const content = await readFile( + "./fixtures/plan-with-deletions.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + const formatter = new TextFormatterUseCase(); + + const plan = await parser.parse(content); + const result = formatter.format(plan); + + expect(result).toContain("1 to destroy"); + expect(result).toContain("aws_s3_bucket.deleted"); + expect(result).toContain("delete"); + }); + + it("should format a plan with mixed changes", async () => { + const content = await readFile( + "./fixtures/plan-with-mixed-changes.json", + "utf-8", + ); + const parser = new ParsePlanUseCase(); + const formatter = new TextFormatterUseCase(); + + const plan = await parser.parse(content); + const result = formatter.format(plan); + + expect(result).toContain("1 to add"); + expect(result).toContain("1 to change"); + expect(result).toContain("1 to destroy"); + + expect(result).toContain("aws_s3_bucket.new"); + expect(result).toContain("aws_s3_bucket.updated"); + expect(result).toContain("aws_s3_bucket.deleted"); + }); + }); +}); diff --git a/features/phase-1.md b/features/phase-1.md index 0e14bd9..225f90a 100644 --- a/features/phase-1.md +++ b/features/phase-1.md @@ -55,37 +55,37 @@ This plan is designed for incremental validation, where each iteration produces --- -### Iteration 2: JSON Plan Parsing & Basic Output ⚠️ +### Iteration 2: JSON Plan Parsing & Basic Output ✅ **Goal**: Parse JSON plan files and display basic information **Tasks**: - ✅ Define domain entities for Terraform plan structure (Plan, ResourceChange, etc.) - ✅ Create parser interface in domain layer - ✅ Implement JSON plan parser use case following CLEAN architecture -- ❌ Create text formatter interface and implementation -- ❌ Use `@actions/core.summary` to output results as action step summary -- ⚠️ Add error handling for malformed JSON with descriptive error messages (partial - throws on JSON.parse but lacks descriptive messages) +- ✅ Create text formatter interface and implementation +- ✅ Use `@actions/core.summary` to output results as action step summary +- ✅ Add error handling for malformed JSON with descriptive error messages -**TDD Approach**: ⚠️ +**TDD Approach**: ✅ 1. ✅ Write failing test for parsing valid JSON plan 2. ✅ Implement parser to make test pass 3. ✅ Write failing test for malformed JSON handling -4. ⚠️ Implement error handling (partial) -5. ❌ Write failing test for text formatting -6. ❌ Implement formatter +4. ✅ Implement error handling +5. ✅ Write failing test for text formatting +6. ✅ Implement formatter 7. ✅ Run `npm test` after each change -**E2E Test**: ❌ -- ❌ Script: `npm run test:e2e:json-parsing` (not found in package.json) -- ❌ Uses Vitest with fixtures for various scenarios (additions, deletions, updates, no changes) -- ❌ Validates correct parsing and change detection -- ❌ Tests error handling with malformed JSON fixtures -- ❌ Uses Chance.js for generating random test data where values don't matter +**E2E Test**: ✅ +- ✅ Script: `npm run test:e2e:json-parsing` +- ✅ Uses Vitest with fixtures for various scenarios (additions, deletions, updates, no changes) +- ✅ Validates correct parsing and change detection +- ✅ Tests error handling with malformed JSON fixtures +- ✅ Uses Chance.js for generating random test data where values don't matter -**Validation**: ❌ -- ❌ Action outputs a text summary of changes for a sample JSON plan -- ❌ Summary includes count of resources being added/changed/deleted -- ❌ Can be verified in GitHub Actions workflow summary using `@github/local-action` +**Validation**: ✅ +- ✅ Action outputs a text summary of changes for a sample JSON plan +- ✅ Summary includes count of resources being added/changed/deleted +- ✅ Can be verified in GitHub Actions workflow summary using `@github/local-action` - ✅ `npx biome check` passes - ✅ All tests pass with `npm test` diff --git a/fixtures/malformed-plan.json b/fixtures/malformed-plan.json new file mode 100644 index 0000000..572686d --- /dev/null +++ b/fixtures/malformed-plan.json @@ -0,0 +1 @@ +{ invalid json } \ No newline at end of file diff --git a/fixtures/plan-with-deletions.json b/fixtures/plan-with-deletions.json new file mode 100644 index 0000000..b483580 --- /dev/null +++ b/fixtures/plan-with-deletions.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "terraform_version": "1.5.0", + "planned_values": { + "root_module": { + "resources": [] + } + }, + "resource_changes": [ + { + "address": "aws_s3_bucket.deleted", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "deleted", + "change": { + "actions": ["delete"], + "before": { + "bucket": "old-bucket", + "acl": "private" + }, + "after": null + } + } + ], + "configuration": {} +} diff --git a/fixtures/plan-with-mixed-changes.json b/fixtures/plan-with-mixed-changes.json new file mode 100644 index 0000000..d5f59c2 --- /dev/null +++ b/fixtures/plan-with-mixed-changes.json @@ -0,0 +1,57 @@ +{ + "format_version": "1.0", + "terraform_version": "1.5.0", + "planned_values": { + "root_module": { + "resources": [] + } + }, + "resource_changes": [ + { + "address": "aws_s3_bucket.new", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "new", + "change": { + "actions": ["create"], + "before": null, + "after": { + "bucket": "new-bucket", + "acl": "private" + } + } + }, + { + "address": "aws_s3_bucket.updated", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "updated", + "change": { + "actions": ["update"], + "before": { + "bucket": "existing-bucket", + "acl": "private" + }, + "after": { + "bucket": "existing-bucket", + "acl": "public-read" + } + } + }, + { + "address": "aws_s3_bucket.deleted", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "deleted", + "change": { + "actions": ["delete"], + "before": { + "bucket": "old-bucket", + "acl": "private" + }, + "after": null + } + } + ], + "configuration": {} +} diff --git a/fixtures/plan-with-updates.json b/fixtures/plan-with-updates.json new file mode 100644 index 0000000..4fdef50 --- /dev/null +++ b/fixtures/plan-with-updates.json @@ -0,0 +1,29 @@ +{ + "format_version": "1.0", + "terraform_version": "1.5.0", + "planned_values": { + "root_module": { + "resources": [] + } + }, + "resource_changes": [ + { + "address": "aws_s3_bucket.updated", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "updated", + "change": { + "actions": ["update"], + "before": { + "bucket": "my-bucket", + "acl": "private" + }, + "after": { + "bucket": "my-bucket", + "acl": "public-read" + } + } + } + ], + "configuration": {} +} diff --git a/package.json b/package.json index 221f7f3..6a3b31c 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "scripts": { "test": "vitest run", "test:watch": "vitest", - "test:e2e": "npm run test:e2e:file-reading", + "test:e2e": "npm run test:e2e:file-reading && npm run test:e2e:json-parsing", "test:e2e:file-reading": "vitest run e2e/file-reading.test.ts", + "test:e2e:json-parsing": "vitest run e2e/json-parsing.test.ts", "build": "ncc build src/index.ts -o dist --minify --no-source-map-register", "lint": "biome check .", "lint:fix": "biome check --write .", diff --git a/src/domain/usecases/ITextFormatter.ts b/src/domain/usecases/ITextFormatter.ts new file mode 100644 index 0000000..df9d15f --- /dev/null +++ b/src/domain/usecases/ITextFormatter.ts @@ -0,0 +1,5 @@ +import type { Plan } from "../entities/Plan"; + +export interface ITextFormatter { + format(plan: Plan): string; +} diff --git a/src/domain/usecases/ParsePlanUseCase.test.ts b/src/domain/usecases/ParsePlanUseCase.test.ts index ae8db72..06f2198 100644 --- a/src/domain/usecases/ParsePlanUseCase.test.ts +++ b/src/domain/usecases/ParsePlanUseCase.test.ts @@ -62,6 +62,70 @@ describe("ParsePlanUseCase", () => { const malformedJson = "{ invalid json }"; const parser = new ParsePlanUseCase(); - await expect(parser.parse(malformedJson)).rejects.toThrow(); + await expect(parser.parse(malformedJson)).rejects.toThrow( + "Invalid JSON in plan file", + ); + }); + + it("should throw descriptive error when plan is missing format_version", async () => { + const invalidPlan = JSON.stringify({ + terraform_version: "1.5.0", + resource_changes: [], + }); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(invalidPlan)).rejects.toThrow( + "Invalid plan structure: missing or invalid required field 'format_version'", + ); + }); + + it("should throw descriptive error when plan is missing terraform_version", async () => { + const invalidPlan = JSON.stringify({ + format_version: "1.0", + resource_changes: [], + }); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(invalidPlan)).rejects.toThrow( + "Invalid plan structure: missing or invalid required field 'terraform_version'", + ); + }); + + it("should throw descriptive error when format_version is empty string", async () => { + const invalidPlan = JSON.stringify({ + format_version: "", + terraform_version: "1.5.0", + resource_changes: [], + }); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(invalidPlan)).rejects.toThrow( + "Invalid plan structure: missing or invalid required field 'format_version'", + ); + }); + + it("should throw descriptive error when terraform_version is empty string", async () => { + const invalidPlan = JSON.stringify({ + format_version: "1.0", + terraform_version: "", + resource_changes: [], + }); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(invalidPlan)).rejects.toThrow( + "Invalid plan structure: missing or invalid required field 'terraform_version'", + ); + }); + + it("should throw descriptive error when plan is missing resource_changes", async () => { + const invalidPlan = JSON.stringify({ + format_version: "1.0", + terraform_version: "1.5.0", + }); + const parser = new ParsePlanUseCase(); + + await expect(parser.parse(invalidPlan)).rejects.toThrow( + "Invalid plan structure: missing required field 'resource_changes'", + ); }); }); diff --git a/src/domain/usecases/ParsePlanUseCase.ts b/src/domain/usecases/ParsePlanUseCase.ts index af852c6..397f44b 100644 --- a/src/domain/usecases/ParsePlanUseCase.ts +++ b/src/domain/usecases/ParsePlanUseCase.ts @@ -20,9 +20,69 @@ type TerraformPlanJson = { export class ParsePlanUseCase implements IPlanParser { async parse(content: string): Promise { - const parsed: TerraformPlanJson = JSON.parse(content); + let parsed: unknown; - const resourceChanges = parsed.resource_changes.map( + try { + parsed = JSON.parse(content); + } catch (error) { + throw new Error( + `Invalid JSON in plan file: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + + if (typeof parsed !== "object" || parsed === null) { + throw new Error("Invalid plan structure: expected an object"); + } + + const planData = parsed as Record; + + if ( + !("format_version" in planData) || + typeof planData.format_version !== "string" || + planData.format_version.trim() === "" + ) { + throw new Error( + "Invalid plan structure: missing or invalid required field 'format_version'", + ); + } + + if ( + !("terraform_version" in planData) || + typeof planData.terraform_version !== "string" || + planData.terraform_version.trim() === "" + ) { + throw new Error( + "Invalid plan structure: missing or invalid required field 'terraform_version'", + ); + } + + if ( + !("resource_changes" in planData) || + planData.resource_changes === undefined || + !Array.isArray(planData.resource_changes) + ) { + throw new Error( + "Invalid plan structure: missing required field 'resource_changes'", + ); + } + + const typedPlan = planData as TerraformPlanJson; + + // Validate resource changes structure + for (const rc of typedPlan.resource_changes) { + if (!rc.address || !rc.type || !rc.name || !rc.change) { + throw new Error( + "Invalid plan structure: resource change missing required fields (address, type, name, or change)", + ); + } + if (!Array.isArray(rc.change.actions)) { + throw new Error( + "Invalid plan structure: resource change actions must be an array", + ); + } + } + + const resourceChanges = typedPlan.resource_changes.map( (rc) => new ResourceChange( rc.address, @@ -35,8 +95,8 @@ export class ParsePlanUseCase implements IPlanParser { ); return new Plan( - parsed.format_version, - parsed.terraform_version, + typedPlan.format_version, + typedPlan.terraform_version, resourceChanges, ); } diff --git a/src/domain/usecases/TextFormatterUseCase.test.ts b/src/domain/usecases/TextFormatterUseCase.test.ts new file mode 100644 index 0000000..5e1188a --- /dev/null +++ b/src/domain/usecases/TextFormatterUseCase.test.ts @@ -0,0 +1,190 @@ +import Chance from "chance"; +import { describe, expect, it } from "vitest"; +import { Plan, ResourceChange } from "../entities/Plan"; +import { TextFormatterUseCase } from "./TextFormatterUseCase"; + +const chance = new Chance(); + +describe("TextFormatterUseCase", () => { + it("should format a plan with no changes", () => { + const formatVersion = chance.word(); + const terraformVersion = chance.word(); + const plan = new Plan(formatVersion, terraformVersion, []); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain("No changes"); + expect(result).toContain("0 to add"); + expect(result).toContain("0 to change"); + expect(result).toContain("0 to destroy"); + }); + + it("should count resources to be added", () => { + const address = chance.word(); + const type = chance.word(); + const name = chance.word(); + const resourceChange = new ResourceChange( + address, + type, + name, + ["create"], + null, + { key: "value" }, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain("1 to add"); + expect(result).toContain("0 to change"); + expect(result).toContain("0 to destroy"); + }); + + it("should count resources to be changed", () => { + const address = chance.word(); + const type = chance.word(); + const name = chance.word(); + const resourceChange = new ResourceChange( + address, + type, + name, + ["update"], + { key: "old" }, + { key: "new" }, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain("0 to add"); + expect(result).toContain("1 to change"); + expect(result).toContain("0 to destroy"); + }); + + it("should count resources to be destroyed", () => { + const address = chance.word(); + const type = chance.word(); + const name = chance.word(); + const resourceChange = new ResourceChange( + address, + type, + name, + ["delete"], + { key: "value" }, + null, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain("0 to add"); + expect(result).toContain("0 to change"); + expect(result).toContain("1 to destroy"); + }); + + it("should handle mixed changes", () => { + const resourceToAdd = new ResourceChange( + chance.word(), + chance.word(), + chance.word(), + ["create"], + null, + { key: "value" }, + ); + const resourceToUpdate = new ResourceChange( + chance.word(), + chance.word(), + chance.word(), + ["update"], + { key: "old" }, + { key: "new" }, + ); + const resourceToDelete = new ResourceChange( + chance.word(), + chance.word(), + chance.word(), + ["delete"], + { key: "value" }, + null, + ); + const plan = new Plan("1.0", "1.5.0", [ + resourceToAdd, + resourceToUpdate, + resourceToDelete, + ]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain("1 to add"); + expect(result).toContain("1 to change"); + expect(result).toContain("1 to destroy"); + }); + + it("should list resources being added", () => { + const address = "aws_s3_bucket.example"; + const type = "aws_s3_bucket"; + const name = "example"; + const resourceChange = new ResourceChange( + address, + type, + name, + ["create"], + null, + { key: "value" }, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain(address); + expect(result).toContain("create"); + }); + + it("should list resources being changed", () => { + const address = "aws_s3_bucket.example"; + const type = "aws_s3_bucket"; + const name = "example"; + const resourceChange = new ResourceChange( + address, + type, + name, + ["update"], + { key: "old" }, + { key: "new" }, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain(address); + expect(result).toContain("update"); + }); + + it("should list resources being destroyed", () => { + const address = "aws_s3_bucket.example"; + const type = "aws_s3_bucket"; + const name = "example"; + const resourceChange = new ResourceChange( + address, + type, + name, + ["delete"], + { key: "value" }, + null, + ); + const plan = new Plan("1.0", "1.5.0", [resourceChange]); + + const formatter = new TextFormatterUseCase(); + const result = formatter.format(plan); + + expect(result).toContain(address); + expect(result).toContain("delete"); + }); +}); diff --git a/src/domain/usecases/TextFormatterUseCase.ts b/src/domain/usecases/TextFormatterUseCase.ts new file mode 100644 index 0000000..e6ff004 --- /dev/null +++ b/src/domain/usecases/TextFormatterUseCase.ts @@ -0,0 +1,66 @@ +import type { Plan } from "../entities/Plan"; +import type { ITextFormatter } from "./ITextFormatter"; + +export class TextFormatterUseCase implements ITextFormatter { + format(plan: Plan): string { + const { additions, changes, deletions } = plan.resourceChanges.reduce( + (acc, rc) => { + if (rc.actions.includes("create")) { + acc.additions.push(rc); + } else if (rc.actions.includes("update")) { + acc.changes.push(rc); + } else if (rc.actions.includes("delete")) { + acc.deletions.push(rc); + } + return acc; + }, + { + additions: [] as typeof plan.resourceChanges, + changes: [] as typeof plan.resourceChanges, + deletions: [] as typeof plan.resourceChanges, + }, + ); + + const addCount = additions.length; + const changeCount = changes.length; + const deleteCount = deletions.length; + + let summary = ""; + + if (addCount === 0 && changeCount === 0 && deleteCount === 0) { + summary += "No changes. Infrastructure is up-to-date.\n\n"; + summary += `Plan: ${addCount} to add, ${changeCount} to change, ${deleteCount} to destroy.\n`; + return summary; + } + + summary += "Terraform will perform the following actions:\n\n"; + + if (additions.length > 0) { + summary += "Resources to be created:\n"; + for (const resource of additions) { + summary += ` + ${resource.address} (${resource.actions.join(", ")})\n`; + } + summary += "\n"; + } + + if (changes.length > 0) { + summary += "Resources to be updated:\n"; + for (const resource of changes) { + summary += ` ~ ${resource.address} (${resource.actions.join(", ")})\n`; + } + summary += "\n"; + } + + if (deletions.length > 0) { + summary += "Resources to be destroyed:\n"; + for (const resource of deletions) { + summary += ` - ${resource.address} (${resource.actions.join(", ")})\n`; + } + summary += "\n"; + } + + summary += `Plan: ${addCount} to add, ${changeCount} to change, ${deleteCount} to destroy.\n`; + + return summary; + } +} diff --git a/src/index.ts b/src/index.ts index 04bf998..5f052bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import * as core from "@actions/core"; +import { ParsePlanUseCase } from "./domain/usecases/ParsePlanUseCase"; import { ReadPlanFileUseCase } from "./domain/usecases/ReadPlanFileUseCase"; +import { TextFormatterUseCase } from "./domain/usecases/TextFormatterUseCase"; import { FilesystemAdapter } from "./infrastructure/adapters/FilesystemAdapter"; import { InputValidator } from "./infrastructure/adapters/InputValidator"; @@ -12,6 +14,8 @@ async function run(): Promise { const validator = new InputValidator(); const fileReader = new FilesystemAdapter(); const readPlanFileUseCase = new ReadPlanFileUseCase(fileReader); + const parsePlanUseCase = new ParsePlanUseCase(); + const textFormatterUseCase = new TextFormatterUseCase(); core.info("Validating plan file path..."); await validator.validatePlanFilePath(planFilePath); @@ -23,11 +27,24 @@ async function run(): Promise { `✓ Successfully read plan file (${planFile.content.length} bytes)`, ); - core.setOutput( - "changes-summary", - `Plan file read successfully: ${planFilePath}`, + core.info("Parsing plan file..."); + const plan = await parsePlanUseCase.parse(planFile.content); + core.info( + `✓ Successfully parsed plan (${plan.resourceChanges.length} resource changes)`, ); + core.info("Formatting plan summary..."); + const formattedSummary = textFormatterUseCase.format(plan); + core.info("✓ Successfully formatted plan summary"); + + // Output to step summary + await core.summary + .addHeading("Infrastructure Changes") + .addCodeBlock(formattedSummary, "terraform") + .write(); + + core.setOutput("changes-summary", formattedSummary); + core.info("✓ Action completed successfully"); } catch (error) { if (error instanceof Error) {