diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..335cd2d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,46 @@ +name: Publish + +on: + push: + branches: + - main + paths: + - deno.json + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # The OIDC ID token is used for authentication with JSR. + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 2 # Need previous commit to compare versions + + - name: Check if version changed + id: check_version + run: | + # Get current version + CURRENT_VERSION=$(jq -r '.version' deno.json) + + # Get previous version + PREVIOUS_VERSION=$(git show HEAD~1:deno.json 2>/dev/null | jq -r '.version // empty' || echo "") + + echo "Current version: $CURRENT_VERSION" + echo "Previous version: $PREVIOUS_VERSION" + + if [ -z "$PREVIOUS_VERSION" ]; then + echo "No previous version found, publishing..." + echo "should_publish=true" >> $GITHUB_OUTPUT + elif [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then + echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION, publishing..." + echo "should_publish=true" >> $GITHUB_OUTPUT + else + echo "Version unchanged ($CURRENT_VERSION), skipping publish" + echo "should_publish=false" >> $GITHUB_OUTPUT + fi + + - name: Publish to JSR + if: steps.check_version.outputs.should_publish == 'true' + run: npx jsr publish diff --git a/deno.json b/deno.json index 220b2b0..98cdd89 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@goog/flow-lens", - "version": "0.1.14", + "version": "0.1.15", "license": "Apache", "exports": "./src/main/main.ts", "imports": { @@ -15,6 +15,9 @@ "xml2js": "npm:xml2js@^0.6.2" }, "tasks": { - "test": "deno test --allow-read --allow-env --allow-write --allow-run" + "test": "deno test --allow-read --allow-env --allow-write --allow-run", + "format": "deno fmt", + "format:check": "deno fmt --check", + "lint": "deno lint" } } diff --git a/src/main/flow_comparator.ts b/src/main/flow_comparator.ts index 455f3d8..b4d324f 100644 --- a/src/main/flow_comparator.ts +++ b/src/main/flow_comparator.ts @@ -60,28 +60,44 @@ export function compareFlows(oldFlow: ParsedFlow, newFlow: ParsedFlow) { /** * Compares two objects recursively. + * + * Implementation of deep equality check requires a lot of boilerplate code. + * This is a simple implementation that works for our use case. */ -// Implementation of deep equality check requires a lot of boilerplate code. -// This is a simple implementation that works for our use case, but it requires -// us to cast the types to any. -// tslint:disable-next-line:no-any -function areEqual(node1: any, node2: any): boolean { +function areEqual(node1: unknown, node2: unknown): boolean { if (node1 === node2) { return true; } - const keys1 = Object.keys(node1); - const keys2 = Object.keys(node2); + if ( + typeof node1 !== OBJECT || + typeof node2 !== OBJECT || + node1 === null || + node2 === null + ) { + return false; + } + + // At this point, TypeScript knows both are non-null objects + const obj1 = node1 as Record; + const obj2 = node2 as Record; + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { - const val1 = node1[key]; - const val2 = node2[key]; + const val1 = obj1[key]; + const val2 = obj2[key]; - if (typeof val1 === OBJECT && typeof val2 === OBJECT) { + if ( + typeof val1 === OBJECT && + typeof val2 === OBJECT && + val1 !== null && + val2 !== null + ) { if (!areEqual(val1, val2)) { return false; } diff --git a/src/main/flow_parser.ts b/src/main/flow_parser.ts index d84d3b2..456fc6a 100644 --- a/src/main/flow_parser.ts +++ b/src/main/flow_parser.ts @@ -109,7 +109,7 @@ export class FlowParser { return this.generateParsedFlow(flowFromXml.Flow); } - private async parseXmlFile(): Promise { + private parseXmlFile(): Promise { return new Promise((resolve, reject) => { new Parser({ explicitArray: false }).parseString( this.flowXml, @@ -621,9 +621,7 @@ function setFlowStart(start: flowTypes.FlowStart | undefined) { return; } if (start.filters) { - start.filters = ensureArray( - start.filters, - ) as flowTypes.FlowRecordFilter[]; + start.filters = ensureArray(start.filters) as flowTypes.FlowRecordFilter[]; } if (start.scheduledPaths) { start.scheduledPaths = ensureArray( @@ -734,9 +732,9 @@ function isCustomError( return (node as flowTypes.FlowCustomError).customErrorMessages !== undefined; } -function isFlowStart( - node: flowTypes.FlowNode, -): node is flowTypes.FlowStart { - return (node as flowTypes.FlowStart).name === START && - (node as flowTypes.FlowStart).label === undefined; +function isFlowStart(node: flowTypes.FlowNode): node is flowTypes.FlowStart { + return ( + (node as flowTypes.FlowStart).name === START && + (node as flowTypes.FlowStart).label === undefined + ); } diff --git a/src/main/flow_to_uml_transformer.ts b/src/main/flow_to_uml_transformer.ts index 6a85b18..478a320 100644 --- a/src/main/flow_to_uml_transformer.ts +++ b/src/main/flow_to_uml_transformer.ts @@ -130,7 +130,7 @@ class Reader implements Filter { let old: string | undefined = undefined; try { old = this.changeDetector.getFileContent(input, "old"); - } catch (error: unknown) { + } catch (_error: unknown) { console.log(ERROR_MESSAGES.previousFlowNotFound(input)); } return { diff --git a/src/main/uml_generator.ts b/src/main/uml_generator.ts index 2f153d0..5f28f24 100644 --- a/src/main/uml_generator.ts +++ b/src/main/uml_generator.ts @@ -473,13 +473,14 @@ export abstract class UmlGenerator { innerNodeContent.push(limit); } - let innerNode = { - id: `${node.name}__LookupDetails`, - type: `sObject: ${node.object}`, - label: "", - content: innerNodeContent, - }; - return [innerNode]; + return [ + { + id: `${node.name}__LookupDetails`, + type: `sObject: ${node.object}`, + label: "", + content: innerNodeContent, + }, + ]; } private getFieldsQueried(node: flowTypes.FlowRecordLookup): string[] { diff --git a/src/main/uml_writer.ts b/src/main/uml_writer.ts index 6d79c20..6238cb7 100644 --- a/src/main/uml_writer.ts +++ b/src/main/uml_writer.ts @@ -57,7 +57,7 @@ export class UmlWriter { if (config.mode === Mode.JSON) { this.writeJsonFile(config); } else if (config.mode === Mode.GITHUB_ACTION) { - await this.writeGithubComment(config); + await this.writeGithubComment(); } else if (config.mode === Mode.MARKDOWN) { this.writeMarkdownFiles(config); } @@ -77,7 +77,7 @@ export class UmlWriter { ); } - private async writeGithubComment(config: RuntimeConfig) { + private async writeGithubComment() { try { const existingComments = await this.githubClient .getAllCommentsForPullRequest(); diff --git a/src/test/argument_processor_test.ts b/src/test/argument_processor_test.ts index 262c9dc..e022f71 100644 --- a/src/test/argument_processor_test.ts +++ b/src/test/argument_processor_test.ts @@ -33,7 +33,7 @@ const INVALID_MODE = "unsupported"; function setupTest( configModifications: (config: RuntimeConfig) => void = () => {}, ) { - let testConfiguration = getTestConfig(); + const testConfiguration = getTestConfig(); configModifications(testConfiguration); return { argumentProcessor: new ArgumentProcessor(testConfiguration), @@ -369,7 +369,9 @@ Deno.test("ArgumentProcessor", async (t) => { }); try { invalidDiagramTool.argumentProcessor.getConfig(); - } catch {} + } catch { + // Error is expected; we check errors via getErrors() instead + } assertEquals(invalidDiagramTool.argumentProcessor.getErrors(), [ ERROR_MESSAGES.githubActionRequiresMermaid, ]); @@ -387,7 +389,9 @@ Deno.test("ArgumentProcessor", async (t) => { }); try { invalidToHash.argumentProcessor.getConfig(); - } catch {} + } catch { + // Error is expected; we check errors via getErrors() instead + } assertEquals(invalidToHash.argumentProcessor.getErrors(), [ ERROR_MESSAGES.githubActionRequiresHeadHash, ]); @@ -405,7 +409,9 @@ Deno.test("ArgumentProcessor", async (t) => { }); try { invalidFromHash.argumentProcessor.getConfig(); - } catch {} + } catch { + // Error is expected; we check errors via getErrors() instead + } assertEquals(invalidFromHash.argumentProcessor.getErrors(), [ ERROR_MESSAGES.githubActionRequiresHeadMinusOne, ]); @@ -423,7 +429,9 @@ Deno.test("ArgumentProcessor", async (t) => { }); try { multipleInvalid.argumentProcessor.getConfig(); - } catch {} + } catch { + // Error is expected; we check errors via getErrors() instead + } assertEquals(multipleInvalid.argumentProcessor.getErrors(), [ ERROR_MESSAGES.githubActionRequiresMermaid, ERROR_MESSAGES.githubActionRequiresHeadHash, diff --git a/src/test/flow_to_uml_transformer_test.ts b/src/test/flow_to_uml_transformer_test.ts index fc9f799..f0a95b0 100644 --- a/src/test/flow_to_uml_transformer_test.ts +++ b/src/test/flow_to_uml_transformer_test.ts @@ -31,26 +31,41 @@ import { import { UmlGeneratorContext } from "../main/uml_generator_context.ts"; import { ERROR_MESSAGES as XML_READER_ERROR_MESSAGES } from "../main/xml_reader.ts"; +type TestableFlowFileChangeDetector = + & Omit< + FlowFileChangeDetector, + "executeGitCommand" + > + & { + executeGitCommand: (args: string[]) => Uint8Array; + }; + +function asTestable( + detector: FlowFileChangeDetector, +): TestableFlowFileChangeDetector { + return detector as unknown as TestableFlowFileChangeDetector; +} + const SAMPLE_FLOW_FILE_PATH = `./src/test/goldens/sample.flow-meta.xml`; const PLANT_UML_SIGNATURE = "skinparam State"; const GENERATOR_CONTEXT = new UmlGeneratorContext(DiagramTool.PLANTUML); -const CHANGE_DETECTOR = new FlowFileChangeDetector(); -Deno.test("FlowToUmlTransformer", async (t) => { - let transformer: FlowToUmlTransformer; - let result: Map; - - await t.step("setup", () => { - // Mock the private method which executes git commands using spyOn - const executeGitCommand = () => { - return new TextEncoder().encode( - Deno.readTextFileSync(SAMPLE_FLOW_FILE_PATH), - ); - }; - // Replace the private method with our mock implementation, using type assertion to access it. - (CHANGE_DETECTOR as any).executeGitCommand = executeGitCommand; - }); +function createMockedDetector(): FlowFileChangeDetector { + const detector = new FlowFileChangeDetector(); + const testableDetector = asTestable(detector); + testableDetector.executeGitCommand = () => { + return new TextEncoder().encode( + Deno.readTextFileSync(SAMPLE_FLOW_FILE_PATH), + ); + }; + return detector; +} + +let transformer: FlowToUmlTransformer | undefined; +let result: Map | undefined; +const changeDetector = createMockedDetector(); +Deno.test("FlowToUmlTransformer", async (t) => { await t.step("should transform a flow file to a UML diagram", async () => { const mockConfig = getTestConfig(); const originalGetInstance = Configuration.getInstance; @@ -58,7 +73,7 @@ Deno.test("FlowToUmlTransformer", async (t) => { transformer = new FlowToUmlTransformer( [SAMPLE_FLOW_FILE_PATH], GENERATOR_CONTEXT, - CHANGE_DETECTOR, + changeDetector, ); result = await transformer.transformToUmlDiagrams(); @@ -100,7 +115,7 @@ Deno.test("FlowToUmlTransformer", async (t) => { transformer = new FlowToUmlTransformer( [fakeFilePath], GENERATOR_CONTEXT, - CHANGE_DETECTOR, + changeDetector, ); result = await transformer.transformToUmlDiagrams(); diff --git a/src/test/github_client_test.ts b/src/test/github_client_test.ts index 60dfbc9..0bdac54 100644 --- a/src/test/github_client_test.ts +++ b/src/test/github_client_test.ts @@ -18,9 +18,17 @@ import { GithubClient, type GithubComment } from "../main/github_client.ts"; import { assertEquals, assertRejects } from "@std/assert"; import { ERROR_MESSAGES } from "../main/github_client.ts"; +type TestableGithubClient = Omit & { + octokit: MockOctokit; +}; + +function asTestable(client: GithubClient): TestableGithubClient { + return client as unknown as TestableGithubClient; +} + // Mock Octokit class MockOctokit { - async request(endpoint: string, data?: unknown) { + request(endpoint: string, data?: unknown) { // Mock response for review comments endpoint if (endpoint.includes("/pulls/") && endpoint.includes("/comments")) { return { @@ -144,7 +152,9 @@ Deno.test("GithubClient", async (t) => { await t.step("writeComment", async (t) => { await t.step("should call Octokit with correct parameters", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient("fake-token", mockContext) as any; + const githubClient = asTestable( + new GithubClient("fake-token", mockContext), + ); githubClient.octokit = mockOctokit; const comment: GithubComment = { @@ -167,10 +177,9 @@ Deno.test("GithubClient", async (t) => { await t.step("should throw an error if not in a PR context", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient( - "fake-token", - invalidContext, - ) as any; + const githubClient = asTestable( + new GithubClient("fake-token", invalidContext), + ); githubClient.octokit = mockOctokit; const comment: GithubComment = { @@ -212,7 +221,9 @@ Deno.test("GithubClient", async (t) => { "should return review comments when in PR context", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient("fake-token", mockContext) as any; + const githubClient = asTestable( + new GithubClient("fake-token", mockContext), + ); githubClient.octokit = mockOctokit; const comments = await githubClient.getAllCommentsForPullRequest(); @@ -227,10 +238,9 @@ Deno.test("GithubClient", async (t) => { await t.step("should throw error when not in PR context", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient( - "fake-token", - invalidContext, - ) as any; + const githubClient = asTestable( + new GithubClient("fake-token", invalidContext), + ); githubClient.octokit = mockOctokit; await assertRejects( @@ -242,9 +252,11 @@ Deno.test("GithubClient", async (t) => { await t.step("should handle API errors", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient("fake-token", mockContext) as any; + const githubClient = asTestable( + new GithubClient("fake-token", mockContext), + ); - mockOctokit.request = async () => { + mockOctokit.request = () => { throw new Error("API error"); }; @@ -262,7 +274,9 @@ Deno.test("GithubClient", async (t) => { await t.step("deleteReviewComment", async (t) => { await t.step("should successfully delete a review comment", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient("fake-token", mockContext) as any; + const githubClient = asTestable( + new GithubClient("fake-token", mockContext), + ); githubClient.octokit = mockOctokit; let errorCaught = false; @@ -278,10 +292,9 @@ Deno.test("GithubClient", async (t) => { await t.step("should throw error when not in PR context", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient( - "fake-token", - invalidContext, - ) as any; + const githubClient = asTestable( + new GithubClient("fake-token", invalidContext), + ); githubClient.octokit = mockOctokit; await assertRejects( @@ -293,9 +306,11 @@ Deno.test("GithubClient", async (t) => { await t.step("should handle API errors", async () => { const mockOctokit = new MockOctokit(); - const githubClient = new GithubClient("fake-token", mockContext) as any; + const githubClient = asTestable( + new GithubClient("fake-token", mockContext), + ); - mockOctokit.request = async () => { + mockOctokit.request = () => { throw new Error("API error"); }; diff --git a/src/test/graphviz_generator_test.ts b/src/test/graphviz_generator_test.ts index 7e42b9e..1587038 100644 --- a/src/test/graphviz_generator_test.ts +++ b/src/test/graphviz_generator_test.ts @@ -15,7 +15,6 @@ */ import { assertEquals, assertStringIncludes } from "@std/assert"; -import type { ParsedFlow } from "../main/flow_parser.ts"; import * as flowTypes from "../main/flow_types.ts"; import { FontColor, diff --git a/src/test/mermaid_generator_test.ts b/src/test/mermaid_generator_test.ts index bde191e..984250f 100644 --- a/src/test/mermaid_generator_test.ts +++ b/src/test/mermaid_generator_test.ts @@ -15,7 +15,7 @@ */ import { assertEquals, assertStringIncludes } from "@std/assert"; -import type { ParsedFlow, Transition } from "../main/flow_parser.ts"; +import type { Transition } from "../main/flow_parser.ts"; import * as flowTypes from "../main/flow_types.ts"; import { MermaidGenerator } from "../main/mermaid_generator.ts"; import { @@ -25,7 +25,6 @@ import { } from "../main/uml_generator.ts"; import { generateMockFlow } from "./utilities/mock_flow.ts"; -// @ts-ignore: Deno types Deno.test("MermaidGenerator", async (t) => { const mockedFlow = generateMockFlow(); const systemUnderTest = new MermaidGenerator(mockedFlow); diff --git a/src/test/uml_generator_context_test.ts b/src/test/uml_generator_context_test.ts index a311340..59d26bd 100644 --- a/src/test/uml_generator_context_test.ts +++ b/src/test/uml_generator_context_test.ts @@ -24,7 +24,7 @@ const parsedFlow: ParsedFlow = { label: "test" }; Deno.test("UmlGeneratorContext", async (t) => { await t.step("should generate diagram using PlantUML", () => { - let generatorContext = new UmlGeneratorContext(DiagramTool.PLANTUML); + const generatorContext = new UmlGeneratorContext(DiagramTool.PLANTUML); const diagram = generatorContext.generateDiagram(parsedFlow); assertStringIncludes(diagram, PLANT_UML_SIGNATURE); }); @@ -32,7 +32,7 @@ Deno.test("UmlGeneratorContext", async (t) => { await t.step( "should default to using PlantUML when an unknown tool is specified", () => { - let generatorContext = new UmlGeneratorContext("fooBar" as DiagramTool); + const generatorContext = new UmlGeneratorContext("fooBar" as DiagramTool); const diagram = generatorContext.generateDiagram(parsedFlow); assertStringIncludes(diagram, PLANT_UML_SIGNATURE); }, diff --git a/src/test/uml_generator_test.ts b/src/test/uml_generator_test.ts index f6b5dd1..fd0f18d 100644 --- a/src/test/uml_generator_test.ts +++ b/src/test/uml_generator_test.ts @@ -101,14 +101,10 @@ class ConcreteUmlGenerator extends UmlGenerator { } } -Deno.test("UmlGenerator", async (t) => { - let systemUnderTest: UmlGenerator; - let mockParsedFlow: ParsedFlow; - - // Setup: initialize test data and system under test - mockParsedFlow = generateMockFlow(); - systemUnderTest = new ConcreteUmlGenerator(mockParsedFlow); +const mockParsedFlow = generateMockFlow(); +const systemUnderTest = new ConcreteUmlGenerator(mockParsedFlow); +Deno.test("UmlGenerator", async (t) => { await t.step("should generate UML with all flow elements", () => { const uml = systemUnderTest.generateUml(); diff --git a/src/test/uml_writer_test.ts b/src/test/uml_writer_test.ts index 15f476e..d541b0e 100644 --- a/src/test/uml_writer_test.ts +++ b/src/test/uml_writer_test.ts @@ -45,7 +45,6 @@ const FLOW_DIFFERENCE_2: FlowDifference = { old: UML_1, new: UML_2, }; -const ENCODING = "utf8"; const OUTPUT_FILE_NAME = "output_file_name"; const FILE_PATH_TO_FLOW_DIFFERENCE = new Map([ @@ -104,7 +103,7 @@ Deno.test("UmlWriter", async (t) => { // Create a mock GithubClient with spy methods const mockGithubClient = { - writeComment: spy(async (_comment: GithubComment) => Promise.resolve()), + writeComment: spy((_comment: GithubComment) => Promise.resolve()), translateToComment: spy( (_body: string, filePath: string): GithubComment => ({ commit_id: "mock_sha", @@ -114,7 +113,7 @@ Deno.test("UmlWriter", async (t) => { }), ), // Add the new methods - getAllCommentsForPullRequest: spy(async () => { + getAllCommentsForPullRequest: spy(() => { return [ { id: 1, @@ -126,7 +125,7 @@ Deno.test("UmlWriter", async (t) => { }, ]; }), - deleteReviewComment: spy(async (_commentId: number) => Promise.resolve()), + deleteReviewComment: spy((_commentId: number) => Promise.resolve()), }; // Set up environment variable for GITHUB_TOKEN diff --git a/src/test/xml_reader_test.ts b/src/test/xml_reader_test.ts index cc929bb..f887894 100644 --- a/src/test/xml_reader_test.ts +++ b/src/test/xml_reader_test.ts @@ -61,7 +61,7 @@ Deno.test("XmlReader", async (t) => { await t.step( "should throw an error when the file path does not exist", - async () => { + () => { const xmlReader = new XmlReader(INVALID_FILE_PATH); assertThrows( () => xmlReader.getXmlFileBody(),