-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b16a269
commit 3f02485
Showing
8 changed files
with
541 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@inlang/cross-sell-ninja": patch | ||
--- | ||
|
||
create @inlang/cross-sell-ninja |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
35
inlang/source-code/cross-sell/cross-sell-ninja/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
194
inlang/source-code/cross-sell/cross-sell-ninja/src/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
}) |
Oops, something went wrong.