Skip to content

Commit

Permalink
init cross-sell-ninja
Browse files Browse the repository at this point in the history
  • Loading branch information
felixhaeberle committed Mar 25, 2024
1 parent b16a269 commit 3f02485
Show file tree
Hide file tree
Showing 8 changed files with 541 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-rocks-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/cross-sell-ninja": patch
---

create @inlang/cross-sell-ninja
67 changes: 67 additions & 0 deletions inlang/source-code/cross-sell/cross-sell-ninja/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Cross-sell Ninja 🥷 package

## Features

- **Workflow Verification**: Ensures that the GitHub Actions workflow incorporating the Ninja i18n lint action is present in a project's `.github/workflows` directory.
- **Automated Workflow Addition**: If not present, the package can automatically add a workflow to incorporate Ninja i18n linting into the project.

## Installation

Add this package to your `dependencies` in `package.json` & install it using `pnpm install`:

```bash
"@inlang/cross-sell-ninja": "workspace:*"
```

## Usage

This module exports two main asynchronous functions:

### `isAdopted(fs: NodeishFilesystem): Promise<boolean>`

Verifies if the Ninja i18n GitHub Action is adopted within the GitHub workflow files of your project.

#### Parameters

- `fs`: A `NodeishFilesystem` object for file system interactions.

#### Returns

- `Promise<boolean>`: `true` if the Ninja i18n GitHub Action workflow is found, `false` otherwise.

### `add(fs: NodeishFilesystem): Promise<void>`

Adds the Ninja i18n GitHub Action workflow to the repository's `.github/workflows` directory if it's not already present.

#### Parameters

- `fs`: A `NodeishFilesystem` object for file system interactions.

## Example

```typescript
import { isAdopted, add } from '@inlang/cross-sell-ninja';
import { NodeishFilesystem } from '@lix-js/fs';

async function ensureNinjaAdoption(fs: NodeishFilesystem) {
const isWorkflowAdopted = await isAdopted(fs);

if (!isWorkflowAdopted) {
// Optionally prompt for user confirmation
const userConfirmed = await promptUser("Do you want to add the Ninja i18n workflow?");

if (userConfirmed) {
await add(fs);
console.log('Ninja i18n workflow added.');
} else {
console.log('User declined to add Ninja i18n workflow.');
}
} else {
console.log('Ninja i18n workflow is already adopted.');
}
}
```

## Contributing

Contributions are highly appreciated! Whether it's feature requests, bug reports, or pull requests, please feel free to contribute. Check out our [Discord](https://discord.gg/CNPfhWpcAa) for community discussions and updates.
35 changes: 35 additions & 0 deletions inlang/source-code/cross-sell/cross-sell-ninja/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@inlang/cross-sell-ninja",
"description": "A package to cross-sell Ninja",
"version": "0.0.0",
"type": "module",
"publishConfig": {
"access": "public"
},
"exports": {
".": "./dist/index.js"
},
"files": [
"./dist",
"./src"
],
"scripts": {
"build": "tsc --build",
"dev": "tsc --watch",
"test": "tsc --noEmit && vitest run --passWithNoTests --coverage",
"lint": "eslint ./src --fix",
"format": "prettier ./src --write",
"clean": "rm -rf ./dist ./node_modules"
},
"devDependencies": {
"@inlang/sdk": "workspace:*",
"@lix-js/fs": "workspace:*",
"@types/js-yaml": "^4.0.9",
"@sinclair/typebox": "^0.31.17",
"@vitest/coverage-v8": "0.33.0",
"js-yaml": "^4.1.0",
"patch-package": "6.5.1",
"typescript": "^5.1.3",
"vitest": "0.33.0"
}
}
194 changes: 194 additions & 0 deletions inlang/source-code/cross-sell/cross-sell-ninja/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import * as yaml from "js-yaml"
import { add, isAdopted } from "./index.js"
import type { NodeishFilesystem } from "@lix-js/fs"

vi.mock("js-yaml", async () => {
const actual = (await vi.importActual("js-yaml")) as any
return {
...actual,
load: vi.fn(actual.load),
dump: vi.fn(actual.dump),
}
})

describe("GitHub Actions Workflow Adoption Checks", () => {
let fsMock: NodeishFilesystem

beforeEach(() => {
fsMock = {
exists: vi.fn(),
readdir: vi.fn(),
// @ts-expect-error
readFile: vi.fn(),
stat: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
}
})

it("detects adoption of Ninja i18n GitHub Action", async () => {
fsMock.exists.mockResolvedValue(true)
// @ts-expect-error
fsMock.readdir.mockResolvedValue(["ninja_i18n.yml"])
// @ts-expect-error
fsMock.readFile.mockResolvedValue(
yaml.dump({
name: "Ninja i18n action",
on: { pull_request_target: {} },
jobs: {
"ninja-i18n": {
name: "Ninja i18n - GitHub Lint Action",
"runs-on": "ubuntu-latest",
steps: [
{
name: "Checkout",
id: "checkout",
uses: "actions/checkout@v4",
},
{
name: "Run Ninja i18n",
id: "ninja-i18n",
uses: "opral/ninja-i18n-action@main",
env: {
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}",
},
},
],
},
},
})
)
// @ts-expect-error
fsMock.stat.mockResolvedValue({ isDirectory: () => false })

await expect(isAdopted({ fs: fsMock })).resolves.toBe(true)
})

it("correctly adds the Ninja i18n GitHub Action workflow", async () => {
fsMock.exists.mockResolvedValue(false)

await add({ fs: fsMock })

expect(fsMock.mkdir).toHaveBeenCalledWith(".github/workflows", { recursive: true })
// @ts-expect-error
const writtenContent = fsMock.writeFile.mock.calls[0][1]
expect(writtenContent).toContain("name: Ninja i18n action")
expect(writtenContent).toContain("uses: opral/ninja-i18n-action@main")
})

it("does not find the action in deep nested directories beyond level 3", async () => {
// Simulate deep directory structure
fsMock.exists.mockResolvedValue(true)
// @ts-expect-error
fsMock.readdir.mockImplementation((path) => {
if (path.endsWith("level1")) return Promise.resolve(["level2"])
if (path.endsWith("level2")) return Promise.resolve(["level3"])
if (path.endsWith("level3")) return Promise.resolve([])
return Promise.resolve(["level1"])
})
// @ts-expect-error
fsMock.stat.mockImplementation((path) =>
Promise.resolve({
isDirectory: () => path.includes("level"),
})
)

await expect(isAdopted({ fs: fsMock })).resolves.toBe(false)
})

it("does not search beyond a depth of 3", async () => {
// @ts-expect-error
fsMock.readdir.mockImplementation((path) => {
// Implement logic to simulate depth, based on the path argument
if (path.endsWith("level3")) return Promise.resolve(["tooDeepDirectory"])
return Promise.resolve(["level1"])
})
// @ts-expect-error
fsMock.stat.mockImplementation((path) =>
Promise.resolve({
isDirectory: () => !path.endsWith(".yml"),
})
)

await expect(isAdopted({ fs: fsMock })).resolves.toBe(false)
// Ensure readdir was called the correct number of times to validate depth control
})

it("returns false if checking directory existence throws an error", async () => {
fsMock.exists.mockRejectedValue(new Error("Filesystem error"))

await expect(isAdopted({ fs: fsMock })).resolves.toBe(false)
})

it("returns true when the action is found in a nested directory within depth limit", async () => {
fsMock.exists.mockResolvedValue(true)
// @ts-expect-error
fsMock.readdir.mockImplementation((path) => {
if (path === ".github/workflows") return Promise.resolve(["level1"])
if (path === ".github/workflows/level1") return Promise.resolve(["level2"])
if (path === ".github/workflows/level1/level2") return Promise.resolve(["ninja_i18n.yml"])
return Promise.resolve([])
})

// @ts-expect-error
fsMock.readFile.mockImplementation((path) => {
if (path === ".github/workflows/level1/level2/ninja_i18n.yml") {
return Promise.resolve(
yaml.dump({
name: "Ninja i18n action",
on: { pull_request_target: {} },
jobs: {
"ninja-i18n": {
"runs-on": "ubuntu-latest",
steps: [
{
name: "Run Ninja i18n",
uses: "opral/ninja-i18n-action@main",
env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
},
],
},
},
})
)
}
return Promise.reject(new Error("File not found"))
})

// @ts-expect-error
fsMock.stat.mockImplementation((path) =>
Promise.resolve({
isDirectory: () => !path.endsWith(".yml"),
})
)

await expect(isAdopted({ fs: fsMock })).resolves.toBe(true)
})

it("returns false and logs an error for malformed YAML content", async () => {
fsMock.exists.mockResolvedValue(true)
// @ts-expect-error
fsMock.readdir.mockResolvedValue(["ninja_i18n.yml"])
// @ts-expect-error
fsMock.readFile.mockResolvedValue("malformed yaml content")
// @ts-expect-error
fsMock.stat.mockResolvedValue({ isDirectory: () => false })

await expect(isAdopted({ fs: fsMock })).resolves.toBe(false)
})

it("handles filesystem errors gracefully in isAdopted function", async () => {
fsMock.exists.mockRejectedValue(new Error("Filesystem error"))

await expect(isAdopted({ fs: fsMock })).resolves.toBe(false)
})

it("handles errors when creating the workflow directory in add function", async () => {
fsMock.exists.mockResolvedValue(false)
// @ts-expect-error
fsMock.mkdir.mockRejectedValue(new Error("Filesystem error"))

await expect(add({ fs: fsMock })).rejects.toThrow("Filesystem error")
})
})
Loading

0 comments on commit 3f02485

Please sign in to comment.