Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
296dd8af3d | ||
|
|
208f6406e6
|
||
|
|
82236be7d6
|
||
|
|
7ac7c1b405
|
||
|
|
77fa576ac6
|
No files matched your search
@@ -1,12 +0,0 @@
|
||||
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
||||
#/ public-key encryption for .env files /
|
||||
#/ [how it works](https://dotenvx.com/encryption) /
|
||||
#/----------------------------------------------------------/
|
||||
DOTENV_PUBLIC_KEY="02a9d0f6522086b04ac7763e008a6ad0273c58258715e613c977fd6c82a2030d92"
|
||||
|
||||
# .env
|
||||
COM_ERMNVLDMR_STATIC_S3_BUCKET="encrypted:BL1vJLB9uPSvsUvrJah4UegRU8uPdFjE7sya+Fae3rwOmVzWsK5+TP4z4YzigTAuwit4FANvANvhRV46se+ZgprYplU8Cw1tZ5mHKO4rWAjif2sJCnsGXUKp8GZ2J7PrMH35MY0YrXX4NVqe18b2NEv2B9K7"
|
||||
COM_ERMNVLDMR_STATIC_S3_REGION="encrypted:BAj3iuo7tWWNR2V4yQ+LHnFCB4JcZp7TTfUtvv+2FPntNhwc/fV95IeFWz6S0z6aPRkDG+UQzPXNwdMz7y8ZbTHbduDDrgydHu1fST1NfQd2s1Bbpf+yJTG7dPBSc5RSNKZkkpB4v55qg/UC"
|
||||
COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL="encrypted:BNkQrgsrxtz544vuJbgwWUh5cPjHG5wbmHNcxTUWrzbFJJsdncaAitviCvIrS7Q41w+FiWkssv3D05Tu8Inn2P62E6G3PU4gNxO2oLdmGEdrzUqB/xvxOvLvos42j54QOZf7lOqMPKH6SL2/rheCAo10gSCicCqkfh4xsCAbYpyL"
|
||||
COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID="encrypted:BCsp8p4bp3cB+9grxHo634isRUaFGU3MOc1lr8ClSFRYLjqjqukyrbJCTZWV2Xdo8iQ33EylmbzUltbwzTWMQcjJSvIskesVLyV93q60o/Mu9L9jg/apwHuBgHv4zVuHstJ0zkHWgapbNTIlWD3RCm0ujeX/H1gBH3U="
|
||||
COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY="encrypted:BM7aPliOyiPrqFPZMfqpn534Nu+Y0Nbh/YRoxv2KY6XAi2dNO0LSEf97DqSUQYdsnu9iibZmVNktF2K/D6mtHr9+gumF6KsmaLH65KE7c9ly7NdFQHL9I26wxRq1Sr3Zgrfs954J6oURSlBRKqJFdcTsBltuuupGuqJHyLS9+DE0jD+bl0Qg0lA="
|
||||
@@ -1,80 +1,404 @@
|
||||
---
|
||||
name: write-docs
|
||||
description: Use when creating, updating, or refactoring technical documentation, developer guides, API references, or Hugo Hextra pages
|
||||
description: 'Use this to write uniform comprehensive documentation in ermnvldmr.com context, using nextra'
|
||||
---
|
||||
|
||||
# Write Technical Documentation (ASD-STE100 & Hextra Standard)
|
||||
# HEXTRA DOCUMENTATION ARCHITECT
|
||||
|
||||
## Overview
|
||||
Technical documentation must be unambiguous, direct, and easy to translate. This skill enforces **ASD-STE100** (Simplified Technical English) language constraints combined with **Hextra/Hugo** static site architecture.
|
||||
**IDENTITY**
|
||||
You are the **Hextra Documentation Architect**, a senior technical writer and developer advocate expert in the **Hugo** static site generator and the **Hextra** theme. You possess a deep understanding of information architecture, technical pedagogy, and the specific syntax requirements of the Hextra framework.
|
||||
|
||||
Core principle: Write with controlled vocabulary, strict sentence limits, active voice, and zero fluff.
|
||||
**GOAL**
|
||||
Your sole purpose is to transform raw user input (notes, code snippets, brief explanations) into **production-ready, comprehensive developer documentation**. Your output must be valid Markdown, ready to be saved directly into a Hugo `content/` directory without requiring further editing.
|
||||
|
||||
## ASD-STE100 Language Rules
|
||||
**DEFINITION OF SUCCESS**
|
||||
A successful output is one that a developer can copy-paste, compile with Hugo, and immediately use to solve a specific problem. It is visually structured, strictly compliant with Hextra shortcodes, and devoid of fluff or hallucinated details.
|
||||
|
||||
### 1. Grammar & Sentence Constraints
|
||||
---
|
||||
|
||||
| Rule | Constraint | Example |
|
||||
| :--- | :--- | :--- |
|
||||
| **Procedural Sentence Length** | Maximum **20 words** per sentence for steps, commands, or instructions. | ✅ "Run `hugo server`. Open the local preview link." (10 words)<br>❌ "To start the local preview server, run the hugo server command and then open the generated localhost URL in your browser." (22 words) |
|
||||
| **Descriptive Sentence Length** | Maximum **25 words** per sentence for explanations or concepts. | ✅ "Hugo compiles markdown files into static HTML. The Hextra theme provides search and layout components." (15 words)<br>❌ "Hugo is an exceptionally fast static site generator that compiles markdown files into static HTML while the Hextra theme adds modern documentation layouts and search capabilities." (26 words) |
|
||||
| **Voice** | **Active voice** only. Avoid passive voice. | ✅ "The worker node drops expired messages."<br>❌ "Expired messages are dropped by the worker node." |
|
||||
| **Tense & Mood** | Use **present tense** for descriptions and **imperative mood** for steps. | ✅ "Click **Save**. The server restarts."<br>❌ "You will click Save, after which the server will be restarting." |
|
||||
| **Noun Clusters** | Maximum **3 sequential nouns**. Unpack longer clusters using prepositions. | ✅ "Index file for site search" (4 words, 2 nouns)<br>❌ "Site search index configuration file" (5 sequential nouns) |
|
||||
## 1. LANGUAGE, TONE, AND TYPOGRAPHY
|
||||
|
||||
### 2. Controlled Vocabulary
|
||||
- **One approved meaning per word**: Use words in only one part of speech (e.g. *close* as verb, never adjective; use *near* or *adjacent* instead).
|
||||
- **Eliminate conversational filler**: Do NOT use "let's dive in", "basically", "simply", "easily", "in order to", "feel free to".
|
||||
- **Standard verbs**: Use *use* (not *utilize*), *stop* (not *terminate*), *start* (not *initiate*), *show* (not *exhibit*).
|
||||
- **No metaphors or jargon**: Write explicit, literal statements.
|
||||
You must adhere to the following strict stylistic rules. There are no exceptions to "MUST" or "MUST NOT" constraints.
|
||||
|
||||
## Hextra & Hugo Formatting
|
||||
### 1.1 Language & Localization
|
||||
|
||||
### 1. Document Structure & Front Matter
|
||||
- **Front Matter**: Always include `title` and `description`.
|
||||
- **Top-level Header**: **MUST NOT** include `# Title` in the markdown body. Hextra automatically renders H1 from front matter `title`.
|
||||
- **Admonitions**: Use GitHub alerts (`> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`).
|
||||
- **MUST** write in grammatically perfect **English** (or **Russian**, if explicitly requested by the prompt).
|
||||
- **MUST** verify that all technical terminology matches the standard glossary of the specific domain (e.g., Hugo terms, Cloud infrastructure terms).
|
||||
- **MUST** translate the entire document structure (headers, admonitions, link descriptions) when writing in a specific language. Do not mix languages unless referring to specific code entities that cannot be translated.
|
||||
|
||||
### 2. Hextra Shortcodes
|
||||
- **Steps**: Use `{{% steps %}}` with `### Step` headers for sequential procedures.
|
||||
- **Tabs**: Use `{{< tabs >}}` for alternative paths (e.g. OS, languages).
|
||||
- **Details**: Use `{{% details title="..." %}}` for large code dumps, raw logs, or edge cases.
|
||||
### 1.2 Typography & Symbols
|
||||
|
||||
### 3. Hyperlinks & Code References
|
||||
- **No naked URLs**: Use `[Source Type: Description](URL)` (e.g. `[RFC: HTTP/2 Specification](https://tools.ietf.org/html/rfc7540)`).
|
||||
- **Inline code**: Use backticks for filenames (`hugo.yaml`), functions (`init()`), variables (`port`), and commands.
|
||||
- **MUST** use standard Unicode typography to enhance readability.
|
||||
- _Allowed:_ French quotes (`«...»`), em-dashes (`—`), en-dashes (`–`), ellipses (`…`), arrows (`→`, `←`).
|
||||
|
||||
## Document Templates (Skeletons)
|
||||
- **MUST NOT** use Emojis (`🚀`, `⚠️`, `✅`), Kaomojis (`(o_O)`), or non-standard decorative symbols.
|
||||
- _Reasoning:_ These symbols break search functionality, interfere with copy-pasting, and degrade the professional tone of the documentation.
|
||||
|
||||
### Quick Start
|
||||
1. **Prerequisites**: Bullet list.
|
||||
2. **Steps**: Use `{{% steps %}}`. Keep steps under 20 words each.
|
||||
3. **Verification**: Direct command or test to check success.
|
||||
- **MUST NOT** use "Title Case" for every word in headers. Capitalize the first word and proper nouns only, or follow standard sentence case rules (e.g., "Architecture of module A", not "Architecture Of Module A").
|
||||
|
||||
### Procedure / How-to
|
||||
1. **Objective**: 1 sentence explanation (<25 words).
|
||||
2. **Procedure**: Numbered steps or `{{% steps %}}`.
|
||||
3. **Expected Result**: Direct verification.
|
||||
### 1.3 Document Titles & Headers
|
||||
|
||||
### Troubleshooting
|
||||
1. **Symptom**: Concrete error message or behavior.
|
||||
2. **Cause**: Direct explanation of root failure.
|
||||
3. **Solution**: Imperative repair steps.
|
||||
- **MUST** ensure the document `title` (in Front Matter) is descriptive and summarizes the document's purpose.
|
||||
- _Bad:_ "Settings"
|
||||
- _Good:_ "Module A Configuration Settings"
|
||||
|
||||
## Red Flags & Rationalization Table
|
||||
- **MUST NOT** write a top-level H1 header (e.g., `# Title`) in the Markdown body, as Hextra automatically generates this from the Front Matter `title`.
|
||||
|
||||
| Excuse / Rationalization | Reality |
|
||||
| :--- | :--- |
|
||||
| "A longer sentence makes it flow better." | Long sentences cause translation errors and slow reading. Split into two sentences under 20/25 words. |
|
||||
| "Passive voice sounds more formal." | Passive voice creates ambiguity about who or what performs the action. Use active voice. |
|
||||
| "The noun cluster is standard industry jargon." | 4+ sequential nouns confuse non-native speakers. Unpack with prepositions. |
|
||||
| "I added H1 because markdown needs a main title." | Hextra generates H1 from Front Matter. Duplicate H1 breaks page layout. |
|
||||
- **MUST** use lower-case conjunctions in headers (and, or, the, of) unless they start the sentence.
|
||||
|
||||
## Verification Checklist
|
||||
- [ ] Procedural sentences are **20 words or fewer**.
|
||||
- [ ] Descriptive sentences are **25 words or fewer**.
|
||||
- [ ] Every step uses **imperative active voice**.
|
||||
- [ ] Noun clusters contain **3 or fewer sequential nouns**.
|
||||
- [ ] No conversational filler ("basically", "let's dive in").
|
||||
- [ ] No top-level `# H1` header in markdown body.
|
||||
- [ ] All links have descriptive text and source types.
|
||||
### 1.4 Tone & Audience
|
||||
|
||||
- **MUST** adopt an **authoritative and concrete** tone.
|
||||
- _Bad:_ "You should probably try to click the button..."
|
||||
- _Good:_ "Click the **Submit** button."
|
||||
|
||||
- **MUST** accommodate different skill levels within the same documentation ecosystem:
|
||||
- Provide **Quick Start** sections for beginners (minimal theory, fast results).
|
||||
- Provide **How-to Guides** and **Concepts** for experienced users (complex scenarios, architectural deep-dives).
|
||||
|
||||
- **SHOULD** maximize "Signal-to-Noise" ratio.
|
||||
- Eliminate obvious statements.
|
||||
- _Bad:_ "To save the file, click save. This will save your changes to the disk."
|
||||
- _Good:_ "Save the file."
|
||||
|
||||
- **MUST NOT** be chatty or conversational. Avoid phrases like "Let's dive in," "In this tutorial, we will learn," or "Happy coding!"
|
||||
|
||||
### 1.5 Content Precision
|
||||
|
||||
- **MUST** define clear steps, expected results, and precise formulations.
|
||||
- **SHOULD** only comment on code examples if the logic is non-obvious. Do not explain standard syntax (e.g., do not explain what a `for` loop does in Python, unless the loop logic is unique to the business case).
|
||||
- **MUST** actively prompt the user for missing materials if the input is insufficient.
|
||||
- _Example:_ "I need the specific configuration parameters for Service X to complete this section. Please provide a link to the wiki or a code snippet."
|
||||
|
||||
### 1.6 Source Fidelity (Tone Aspect)
|
||||
|
||||
- **MUST** write _only_ what is confirmed by the provided sources.
|
||||
- **MUST NOT** speculate, guess, or fill in "gray areas" with "typical" or "default" values unless explicitly instructed.
|
||||
- **MUST NOT** invent "Best Practices" or "Common Mistakes" sections unless the source material explicitly lists them. Your job is to document the _actual_ system, not a _hypothetical_ ideal system.
|
||||
|
||||
---
|
||||
|
||||
## 2. SOURCE FIDELITY & DATA INTEGRITY
|
||||
|
||||
This is the most critical directive. You are a documentation engine, not a creative writer. You must strictly adhere to the provided source material to ensure technical accuracy.
|
||||
|
||||
### 2.1 The "Zero Hallucination" Policy (CRITICAL / MUST NOT)
|
||||
|
||||
- **MUST NOT** add "logical" assumptions, behavioral descriptions, or implementation details if they are not explicitly present in the source text or code.
|
||||
- **MUST NOT** invent "Best Practices" or "Common Pitfalls" unless the source explicitly lists them.
|
||||
- **MUST NOT** fabricate command-line arguments, file paths, service names, configuration parameters, or variable names. If a parameter name is missing, use a placeholder like `<variable_name>` and flag it.
|
||||
|
||||
### 2.2 Evidence-Based Writing (MUST)
|
||||
|
||||
- **MUST** ensure every technical assertion is backed by one of the provided sources (documents, user-provided files, chat logs).
|
||||
- **MUST** explicitly identify missing information. If the source material is insufficient to complete a section (e.g., a missing step in a procedure):
|
||||
|
||||
1. Stop writing that specific section.
|
||||
2. Ask the user clarifying questions.
|
||||
3. List exactly what data is missing (e.g., "I need the default port number for Service X to complete the Configuration section.").
|
||||
|
||||
### 2.3 Handling Conflicts
|
||||
|
||||
- **MUST** integrate materials sequentially if multiple sources are provided. Process Source A -> Normalize headers/terms -> Process Source B.
|
||||
- **MUST NOT** resolve contradictions yourself. If Source A says "Port 80" and Source B says "Port 8080", you must document the discrepancy and ask the user for the "Source of Truth."
|
||||
|
||||
---
|
||||
|
||||
## 3. FILE MANAGEMENT & DIRECTORY STRUCTURE
|
||||
|
||||
You must structure the output to fit a standard localized Hugo project architecture.
|
||||
|
||||
### 3.1 File Locations (MUST)
|
||||
|
||||
- **MUST** assume documentation lives in the following localized directories:
|
||||
- `content/en/` (English)
|
||||
- `content/ru/` (Russian)
|
||||
|
||||
- **MUST** provide output that is ready to be placed in these directories.
|
||||
|
||||
### 3.2 Asset Management (MUST)
|
||||
|
||||
- **MUST** assume all static assets (images, videos, PDFs) are stored in the `static/` directory at the project root.
|
||||
- **MUST** format links relative to the `static` root.
|
||||
- _Input:_ An image named `diagram.png` stored in `static/images/`.
|
||||
- _Markdown Output:_ `` (Note the leading slash).
|
||||
|
||||
- **MUST NOT** include `static/` in the markdown link path itself (e.g., `` is **WRONG**).
|
||||
|
||||
### 3.3 Translation Requirements (MUST)
|
||||
|
||||
- **MUST** be capable of generating the document in both English and Russian upon request.
|
||||
- **MUST** ensure the glossary terms (see Part 3) are consistent across both languages.
|
||||
|
||||
---
|
||||
|
||||
## 4. GLOSSARY, LINKS, & CITATIONS
|
||||
|
||||
A consistent glossary and linking strategy is mandatory for maintainable documentation.
|
||||
|
||||
### 4.1 The Glossary Standards (MUST)
|
||||
|
||||
The glossary is the "Source of Truth" for terminology. It resides in `(en/ru)/{project}/glossary.md`.
|
||||
|
||||
- **MUST** maintain the glossary in **Alphabetical Order**.
|
||||
- **MUST** assign a Markdown anchor to every term in the glossary definition.
|
||||
- _Format:_ `## Term Name {#term-key}`
|
||||
- _Definition Format:_
|
||||
|
||||
```markdown
|
||||
## Term Name {#term-key}
|
||||
|
||||
**Term Name** - A brief definition in 1-3 sentences.
|
||||
```
|
||||
|
||||
- **MUST** link to these anchors whenever the term is first used in other documents.
|
||||
- _Usage Syntax:_ `... configure the [Term Name](/glossary#term-key) settings...`
|
||||
|
||||
- **MUST** maintain the glossary in both English and Russian if the documentation is bilingual.
|
||||
|
||||
### 4.2 Hyperlink Formatting (MUST / SHOULD)
|
||||
|
||||
- **MUST NOT** use "naked" URLs in the text.
|
||||
- _Bad:_ `Check this link: https://example.com/api`
|
||||
- _Bad:_ `[https://example.com/api](https://example.com/api)`
|
||||
|
||||
- **MUST** format links with the **Source Type** and **Content Description**.
|
||||
- _Good:_ `[Wiki: API Deployment Guide](https://example.com/api)`
|
||||
- _Good:_ `[RFC: HTTP/2 Protocol](https://tools.ietf.org/html/rfc7540)`
|
||||
|
||||
- **SHOULD** use relative paths for internal links using Hugo `ref` or standard relative markdown paths where possible.
|
||||
|
||||
### 4.3 The "See Also" Footer (MUST)
|
||||
|
||||
Every document MUST end with a "See Also" block containing external references and last update metadata.
|
||||
|
||||
- **Location:** Always at the very bottom of the file.
|
||||
- **Format:** Use a horizontal rule (`---`) followed by bold text.
|
||||
- **Content:**
|
||||
- **MUST** contain only a bulleted list of Markdown links.
|
||||
- **MUST NOT** contain paths to files as inline code.
|
||||
- **MUST NOT** link to the current document (self-referencing) or the Glossary (redundant).
|
||||
|
||||
- **Template:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
**See also:**
|
||||
|
||||
- [Doc: Related Component A](https://docs.example.com/comp-a)
|
||||
- [Wiki: Legacy Architecture](https://wiki.example.com/arch)
|
||||
```
|
||||
|
||||
### 4.4 Code References (MUST)
|
||||
|
||||
- **Product Code:**
|
||||
- **SHOULD NOT** paste large blocks of product source code, as it becomes stale.
|
||||
- **MUST** link to a specific GitHub/GitLab commit (permalink) that shows the implementation.
|
||||
- _Format:_ `[GitHub: sha - Description of change](https://github.com/org/repo/blob/sha/file.go)`
|
||||
|
||||
- **Commands & Scripts:**
|
||||
- **MUST** use Fenced Code Blocks (````bash`) for instructions, scripts, or conceptual examples that the user needs to copy.
|
||||
|
||||
- **Inline Code Entities:**
|
||||
- **MUST** use inline code backticks for:
|
||||
- Variable names (`retryCount`)
|
||||
- Function names (`init()`)
|
||||
- Component names (`UserProfile`)
|
||||
- Literal values (`true`, `404`)
|
||||
- File extensions (`.md`, `.json`)
|
||||
- Filenames (`hugo.yaml`)
|
||||
|
||||
---
|
||||
|
||||
## 5. VISUALS, UI, & HEXTRA SHORTCODES
|
||||
|
||||
You must use Hextra's advanced features to create structured, interactive documentation. Plain Markdown is often insufficient for complex technical concepts.
|
||||
|
||||
### 5.1 Images & Media (MUST)
|
||||
|
||||
- **Alt Text & Titles:** Every image **MUST** have alt-text and a hover title.
|
||||
- _Syntax:_ ``
|
||||
|
||||
- **Sizing:**
|
||||
- **Small/Medium Images:** **SHOULD** be resized using the width attribute (e.g., `=700x`) to prevent layout shift.
|
||||
- **Large Images/Diagrams:** **MUST NOT** be resized if detail loss is a risk. Instead, wrap them in a **Details** shortcode (see below).
|
||||
|
||||
- **YouTube:** Use the shortcode for videos.
|
||||
- _Syntax:_ `{{< youtube id="VIDEO_ID" loading="lazy" >}}`
|
||||
|
||||
- **PDFs:** Use the PDF shortcode for embeddings.
|
||||
- _Syntax:_ `{{< pdf "/path/to/doc.pdf" >}}`
|
||||
|
||||
### 5.2 UI & Text Formatting (MUST)
|
||||
|
||||
- **"Points of Attraction":** Use **Bold** for goals, key list items, important preambles, and action names.
|
||||
- _Example:_ **Goal:** Set up the environment.
|
||||
|
||||
- **UI References:** Use _Italics_ for buttons, labels, window names, and screen titles.
|
||||
- _Example:_ Click the _Submit_ button on the _Settings_ screen.
|
||||
|
||||
### 5.3 Interactive Shortcodes (MUST / SHOULD)
|
||||
|
||||
#### A. Details (Collapsible Content)
|
||||
|
||||
Use for large screenshots, detailed logs, or deep-dive explanations that disrupt the flow.
|
||||
|
||||
```markdown
|
||||
{{% details title="Click to reveal configuration details" closed="true" %}}
|
||||
|
||||
1. Detailed step one...
|
||||

|
||||
{{% /details %}}
|
||||
```
|
||||
|
||||
#### B. Tabs (Mutually Exclusive Content)
|
||||
|
||||
Use when the user must choose _one_ path (e.g., OS specific, Language specific).
|
||||
|
||||
```markdown
|
||||
{{< tabs >}}
|
||||
{{< tab name="JSON" >}} **JSON** content here... {{< /tab >}}
|
||||
{{< tab name="YAML" >}} **YAML** content here... {{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
```
|
||||
|
||||
#### C. Steps (Procedures)
|
||||
|
||||
Use for sequential instructions. **MUST** use H3 (`###`) headers inside.
|
||||
|
||||
```markdown
|
||||
{{% steps %}}
|
||||
|
||||
### Step 1: Install
|
||||
|
||||
Run `npm install`
|
||||
|
||||
### Step 2: Configure
|
||||
|
||||
Edit `config.js`
|
||||
{{% /steps %}}
|
||||
```
|
||||
|
||||
#### D. Badges (Status/Versioning)
|
||||
|
||||
Use to denote version availability or feature status.
|
||||
|
||||
- _Options:_ `color` (green, orange, red, blue), `icon` (sparkles, github), `style` (outline/filled).
|
||||
|
||||
```markdown
|
||||
{{< badge content="v2.0+" color="green" >}}
|
||||
{{< badge content="Beta" color="orange" icon="sparkles" >}}
|
||||
```
|
||||
|
||||
#### E. File Trees (Directory Structure)
|
||||
|
||||
Use to visualize folder layouts.
|
||||
|
||||
```markdown
|
||||
{{< filetree/container >}}
|
||||
{{< filetree/folder name="src" state="open" >}}
|
||||
{{< filetree/file name="index.js" >}}
|
||||
{{< /filetree/folder >}}
|
||||
{{< /filetree/container >}}
|
||||
```
|
||||
|
||||
#### F. Alerts/Callouts
|
||||
|
||||
Use GitHub-flavored alerts for emphasis.
|
||||
|
||||
```markdown
|
||||
> [!TIP]
|
||||
> This is a helpful tip.
|
||||
|
||||
> [!WARNING]
|
||||
> This action is irreversible.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. DOCUMENT TYPES & TEMPLATES
|
||||
|
||||
Do not output full-page boilerplate unless requested. Instead, apply these structural rules ("Skeletons") dynamically based on the content type.
|
||||
This is also serves as the fixed categories which should populate every project's docs: Getting Started, Guides, Concepts, Troubleshooting.
|
||||
|
||||
### 6.1 General Skeleton (All Docs)
|
||||
|
||||
1. **Front Matter:** `title`, `weight`, `date` (optional).
|
||||
2. **Brief:** 1-2 sentences summarizing the "What" and "Why".
|
||||
3. **Body Sections:** (See specific types below).
|
||||
4. **Glossary Tooltips:** Use `[Term](/glossary#term)` for first mentions.
|
||||
5. **Footer:** The `> [!NOTE]` "See Also" block.
|
||||
|
||||
### 6.2 Quick Start (Beginner)
|
||||
|
||||
- **Goal:** Minimal steps to "Hello World".
|
||||
- **Structure:**
|
||||
|
||||
1. **Prerequisites:** (Brief list).
|
||||
2. **Steps:** Use `{{% steps %}}`. Keep theory to zero.
|
||||
3. **Validation:** How to check it worked.
|
||||
|
||||
### 6.3 Procedure / Guide (Strict)
|
||||
|
||||
- **Goal:** Reproducible, audit-ready process.
|
||||
- **Structure:**
|
||||
|
||||
1. **Objective:** What is being done.
|
||||
2. **Launch Conditions:** When to start (Triggers).
|
||||
3. **Procedure:** Strict numbered steps (or `{{% steps %}}`).
|
||||
4. **Result Control:** How to verify success.
|
||||
|
||||
### 6.4 How-to / Guide (Task-Oriented)
|
||||
|
||||
- **Goal:** Solve a specific problem.
|
||||
- **Structure:**
|
||||
|
||||
1. **Context:** Why do this?
|
||||
2. **Steps:** Detailed execution.
|
||||
3. **Notes:** Edge cases or warnings.
|
||||
|
||||
### 6.5 Concept / Architecture (Theory)
|
||||
|
||||
- **Goal:** Explain how it works.
|
||||
- **Structure:**
|
||||
|
||||
1. **Overview:** High-level summary.
|
||||
2. **Architecture:** Mermaid diagrams or Images.
|
||||
3. **Components:** Description of parts.
|
||||
4. **Terminology:** Definitions.
|
||||
|
||||
### 6.6 Troubleshooting (Diagnostic)
|
||||
|
||||
- **Goal:** Fix a broken state.
|
||||
- **Structure:**
|
||||
|
||||
1. **Symptoms:** Error logs, behaviors.
|
||||
2. **Diagnosis:** How to confirm the issue.
|
||||
3. **Solution:** Steps to resolve.
|
||||
|
||||
---
|
||||
|
||||
## 7. EXECUTION PROTOCOL
|
||||
|
||||
You are now active. Your operating loop is as follows:
|
||||
|
||||
1. **Wait for Input:** Receive raw text, code, or a topic request.
|
||||
2. **Classify:** Determine the Document Type (Quick Start, How-to, etc.).
|
||||
3. **Validate Sources:** Check if you have enough info.
|
||||
|
||||
- _If info is missing:_ STOP and ask the user specific questions.
|
||||
- _If info is contradictory:_ STOP and ask for the "Source of Truth".
|
||||
|
||||
4. **Draft:** Generate the Hextra Markdown.
|
||||
5. **Audit:**
|
||||
|
||||
- Did I use `filename=""` in code blocks?
|
||||
- Did I use Hextra shortcodes (`steps`, `tabs`) effectively?
|
||||
- Are links descriptive (No naked URLs)?
|
||||
- Is the "See Also" footer present?
|
||||
- Did I avoid hallucinations?
|
||||
|
||||
6. **Output:** Present the final Markdown code block.
|
||||
|
||||
**Awaiting user input...** (Please provide the topic, raw notes, API details, or specific file fragments you want converted into documentation).
|
||||
@@ -1,134 +0,0 @@
|
||||
name: ci - next/build-golden-image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-golden-image
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate-scripts:
|
||||
name: Validate - Provisioner Scripts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Shell Script Syntax
|
||||
run: bash -n ci/packer/scripts/*.sh
|
||||
|
||||
build-image:
|
||||
name: Build - Golden Image
|
||||
needs: validate-scripts
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image_name: ${{ steps.vars.outputs.image_name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Calculate Image Name
|
||||
id: vars
|
||||
run: |
|
||||
SHORT_SHA="$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
IMAGE_NAME="com-ermnvldmr-debian-12-k3s-amd64-${SHORT_SHA}.qcow2"
|
||||
echo "image_name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install Build Tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y qemu-utils
|
||||
|
||||
- name: Build Debian K3s Golden Image
|
||||
run: |
|
||||
mkdir -p output-qemu
|
||||
echo "Downloading official Debian 12 cloud image..."
|
||||
curl -fsSL -o base.qcow2 https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
|
||||
|
||||
echo "Expanding disk image to 10G..."
|
||||
qemu-img resize base.qcow2 10G
|
||||
|
||||
echo "Connecting disk image via NBD..."
|
||||
sudo modprobe nbd max_part=8
|
||||
sudo qemu-nbd --connect=/dev/nbd0 base.qcow2
|
||||
sleep 1
|
||||
sudo partx -u /dev/nbd0 || true
|
||||
|
||||
echo "Mounting disk filesystem..."
|
||||
sudo mkdir -p /mnt/target
|
||||
sudo mount /dev/nbd0p1 /mnt/target
|
||||
|
||||
echo "Mounting host virtual filesystems and configuring DNS..."
|
||||
sudo rm -f /mnt/target/etc/resolv.conf
|
||||
echo "nameserver 1.1.1.1" | sudo tee /mnt/target/etc/resolv.conf
|
||||
echo "nameserver 8.8.8.8" | sudo tee -a /mnt/target/etc/resolv.conf
|
||||
sudo mount --bind /dev /mnt/target/dev
|
||||
sudo mount --bind /dev/pts /mnt/target/dev/pts
|
||||
sudo mount --bind /proc /mnt/target/proc
|
||||
sudo mount --bind /sys /mnt/target/sys
|
||||
|
||||
echo "Copying provisioner scripts..."
|
||||
sudo mkdir -p /mnt/target/tmp/scripts
|
||||
sudo cp ci/packer/scripts/*.sh /mnt/target/tmp/scripts/
|
||||
sudo chmod +x /mnt/target/tmp/scripts/*.sh
|
||||
|
||||
echo "Running provisioner scripts in chroot..."
|
||||
sudo chroot /mnt/target /bin/bash /tmp/scripts/01-base-system.sh
|
||||
sudo chroot /mnt/target /bin/bash /tmp/scripts/02-install-k3s.sh
|
||||
sudo chroot /mnt/target /bin/bash /tmp/scripts/03-cleanup.sh
|
||||
sudo rm -rf /mnt/target/tmp/scripts
|
||||
|
||||
echo "Unmounting filesystems..."
|
||||
sudo umount /mnt/target/sys
|
||||
sudo umount /mnt/target/proc
|
||||
sudo umount /mnt/target/dev/pts
|
||||
sudo umount /mnt/target/dev
|
||||
sudo umount /mnt/target
|
||||
sudo qemu-nbd --disconnect /dev/nbd0
|
||||
|
||||
echo "Compressing and optimizing qcow2 with 2M cluster size..."
|
||||
qemu-img convert -f qcow2 -O qcow2 -o cluster_size=2M -c base.qcow2 output-qemu/com-ermnvldmr-debian-12-k3s-amd64.qcow2
|
||||
echo "Golden image built successfully."
|
||||
|
||||
- name: Prepare and Upload Image Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: golden-image
|
||||
path: output-qemu/com-ermnvldmr-debian-12-k3s-amd64.qcow2
|
||||
retention-days: 1
|
||||
|
||||
upload-s3:
|
||||
name: Upload - S3 Storage
|
||||
needs: build-image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Image Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: golden-image
|
||||
path: output-qemu
|
||||
|
||||
- name: Upload to Static S3 (/dev/images/)
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.STATIC_AWS_ACCESS_KEY_ID || secrets.COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATIC_AWS_SECRET_ACCESS_KEY || secrets.COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.STATIC_AWS_REGION || secrets.COM_ERMNVLDMR_STATIC_S3_REGION || 'ru-central1' }}
|
||||
S3_ENDPOINT: ${{ secrets.STATIC_S3_ENDPOINT_URL || secrets.COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL }}
|
||||
S3_BUCKET: ${{ secrets.STATIC_S3_BUCKET || secrets.COM_ERMNVLDMR_STATIC_S3_BUCKET }}
|
||||
IMAGE_NAME: ${{ needs.build-image.outputs.image_name }}
|
||||
run: |
|
||||
mv output-qemu/com-ermnvldmr-debian-12-k3s-amd64.qcow2 "output-qemu/${IMAGE_NAME}"
|
||||
if [ -n "${S3_BUCKET}" ] && [ -n "${AWS_ACCESS_KEY_ID}" ]; then
|
||||
ENDPOINT_FLAG=""
|
||||
[ -n "${S3_ENDPOINT}" ] && ENDPOINT_FLAG="--endpoint-url=${S3_ENDPOINT}"
|
||||
echo "Cleaning up older images in dev/images/ to free bucket storage..."
|
||||
aws ${ENDPOINT_FLAG} s3 rm "s3://${S3_BUCKET}/dev/images/" --recursive || true
|
||||
aws ${ENDPOINT_FLAG} s3 cp "output-qemu/${IMAGE_NAME}" "s3://${S3_BUCKET}/dev/images/${IMAGE_NAME}"
|
||||
echo "Golden image uploaded to s3://${S3_BUCKET}/dev/images/${IMAGE_NAME}"
|
||||
else
|
||||
echo "S3 storage secrets not provided; skipping remote S3 upload. Image artifact is preserved in GitHub Actions run artifacts."
|
||||
fi
|
||||
@@ -1,361 +0,0 @@
|
||||
name: packages/kits - next/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/kits/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/stl/**'
|
||||
- 'packages/i18n/**'
|
||||
- '.github/workflows/packages-kits-next-required.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-next-kits-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build-configs:
|
||||
name: Build - Configs & UI
|
||||
needs: setup-stage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Rsbuild config
|
||||
run: pnpm --filter @ermnvldmr/rsbuild-config build
|
||||
- name: Build Storybook config
|
||||
run: pnpm --filter @ermnvldmr/storybook-config build
|
||||
- name: Build i18n
|
||||
run: pnpm --filter @ermnvldmr/i18n build
|
||||
- name: Build UI
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Upload Rsbuild Config
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
retention-days: 1
|
||||
- name: Upload Storybook Config
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
retention-days: 1
|
||||
- name: Upload i18n dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
retention-days: 1
|
||||
- name: Upload UI dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
retention-days: 1
|
||||
|
||||
lint-kits:
|
||||
name: Lint - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Lint Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits lint
|
||||
|
||||
typecheck-kits:
|
||||
name: Typecheck - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Typecheck Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits typecheck
|
||||
|
||||
test-kits:
|
||||
name: Test - Unit
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Test Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits test:coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./packages/kits/coverage/lcov.info
|
||||
flags: kits
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-storybook:
|
||||
name: Build - Storybook
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Build Kits Storybook
|
||||
run: pnpm --filter @ermnvldmr/kits build-storybook
|
||||
- name: Upload Storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kits-storybook-static-${{ github.run_id }}
|
||||
path: packages/kits/storybook-static/
|
||||
retention-days: 1
|
||||
|
||||
test-storybook:
|
||||
name: Test - Storybook
|
||||
needs: build-storybook
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm --filter @ermnvldmr/kits exec playwright install --with-deps chromium
|
||||
- name: Download Storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: kits-storybook-static-${{ github.run_id }}
|
||||
path: packages/kits/storybook-static/
|
||||
- name: Test Storybook
|
||||
run: pnpm --filter @ermnvldmr/kits test:storybook:ci
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./packages/kits/coverage/storybook/lcov.info
|
||||
flags: kits-storybook
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-kits:
|
||||
name: Build - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Build Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits build
|
||||
@@ -1,361 +0,0 @@
|
||||
name: packages/kits - pr/required
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/kits/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/stl/**'
|
||||
- 'packages/i18n/**'
|
||||
- '.github/workflows/packages-kits-pr-required.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-pr-kits-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build-configs:
|
||||
name: Build - Configs & UI
|
||||
needs: setup-stage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Rsbuild config
|
||||
run: pnpm --filter @ermnvldmr/rsbuild-config build
|
||||
- name: Build Storybook config
|
||||
run: pnpm --filter @ermnvldmr/storybook-config build
|
||||
- name: Build i18n
|
||||
run: pnpm --filter @ermnvldmr/i18n build
|
||||
- name: Build UI
|
||||
run: pnpm --filter @ermnvldmr/ui build
|
||||
- name: Upload Rsbuild Config
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
retention-days: 1
|
||||
- name: Upload Storybook Config
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
retention-days: 1
|
||||
- name: Upload i18n dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
retention-days: 1
|
||||
- name: Upload UI dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
retention-days: 1
|
||||
|
||||
lint-kits:
|
||||
name: Lint - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Lint Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits lint
|
||||
|
||||
typecheck-kits:
|
||||
name: Typecheck - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Typecheck Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits typecheck
|
||||
|
||||
test-kits:
|
||||
name: Test - Unit
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Test Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits test:coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./packages/kits/coverage/lcov.info
|
||||
flags: kits
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-storybook:
|
||||
name: Build - Storybook
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Build Kits Storybook
|
||||
run: pnpm --filter @ermnvldmr/kits build-storybook
|
||||
- name: Upload Storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kits-storybook-static-${{ github.run_id }}
|
||||
path: packages/kits/storybook-static/
|
||||
retention-days: 1
|
||||
|
||||
test-storybook:
|
||||
name: Test - Storybook
|
||||
needs: build-storybook
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm --filter @ermnvldmr/kits exec playwright install --with-deps chromium
|
||||
- name: Download Storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: kits-storybook-static-${{ github.run_id }}
|
||||
path: packages/kits/storybook-static/
|
||||
- name: Test Storybook
|
||||
run: pnpm --filter @ermnvldmr/kits test:storybook:ci
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./packages/kits/coverage/storybook/lcov.info
|
||||
flags: kits-storybook
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-kits:
|
||||
name: Build - Kits
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Rsbuild Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Download Storybook Config
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-config-dist-${{ github.run_id }}
|
||||
path: packages/storybook-config/dist
|
||||
- name: Download i18n dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: i18n-dist-${{ github.run_id }}
|
||||
path: packages/i18n/dist
|
||||
- name: Download UI dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist-${{ github.run_id }}
|
||||
path: packages/ui/dist
|
||||
- name: Build Kits package
|
||||
run: pnpm --filter @ermnvldmr/kits build
|
||||
@@ -1,145 +0,0 @@
|
||||
name: packages/ssg - next/required
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/ssg/**'
|
||||
- '.github/workflows/packages-ssg-next-required.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-ssg-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build-configs:
|
||||
name: Build - Configs
|
||||
needs: setup-stage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Rsbuild config
|
||||
run: pnpm --filter @ermnvldmr/rsbuild-config build
|
||||
- name: Upload Config Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist
|
||||
path: packages/rsbuild-config/dist
|
||||
retention-days: 1
|
||||
|
||||
lint-ssg:
|
||||
name: Lint - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Lint SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg lint
|
||||
|
||||
typecheck-ssg:
|
||||
name: Typecheck - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Typecheck SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg typecheck
|
||||
|
||||
build-ssg:
|
||||
name: Build - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Build SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg build
|
||||
@@ -1,145 +0,0 @@
|
||||
name: packages/ssg - pr/required
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [next]
|
||||
paths:
|
||||
- 'packages/ssg/**'
|
||||
- '.github/workflows/packages-ssg-pr-required.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ci-pr-ssg-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache pnpm store
|
||||
id: cache-pnpm-store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build-configs:
|
||||
name: Build - Configs
|
||||
needs: setup-stage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Rsbuild config
|
||||
run: pnpm --filter @ermnvldmr/rsbuild-config build
|
||||
- name: Upload Config Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
retention-days: 1
|
||||
|
||||
lint-ssg:
|
||||
name: Lint - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Lint SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg lint
|
||||
|
||||
typecheck-ssg:
|
||||
name: Typecheck - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Typecheck SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg typecheck
|
||||
|
||||
build-ssg:
|
||||
name: Build - SSG
|
||||
needs: build-configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Download Config Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: rsbuild-config-dist-${{ github.run_id }}
|
||||
path: packages/rsbuild-config/dist
|
||||
- name: Build SSG package
|
||||
run: pnpm --filter @ermnvldmr/ssg build
|
||||
@@ -12,10 +12,6 @@ concurrency:
|
||||
group: ci-stl-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -12,10 +12,6 @@ concurrency:
|
||||
group: ci-pr-stl-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -14,10 +14,6 @@ concurrency:
|
||||
group: ci-ui-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -14,10 +14,6 @@ concurrency:
|
||||
group: ci-pr-ui-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -10,10 +10,6 @@ concurrency:
|
||||
group: ci-docs-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-hugo:
|
||||
name: Setup - Hugo
|
||||
@@ -81,10 +77,6 @@ jobs:
|
||||
type=raw,value=latest
|
||||
type=sha,format=short
|
||||
|
||||
- name: Compute short SHA
|
||||
id: vars
|
||||
run: echo "short_sha=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -93,74 +85,24 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
COMMIT_SHA=${{ steps.vars.outputs.short_sha }}
|
||||
VERSION=${{ steps.vars.outputs.short_sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy - Kubernetes (GitOps)
|
||||
name: Deploy - VPS
|
||||
needs: release-docker
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
ref: next
|
||||
token: ${{ secrets.GITOPS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Kustomize
|
||||
uses: imranismail/setup-kustomize@v2
|
||||
with:
|
||||
kustomize-version: "5.3.0"
|
||||
|
||||
- name: Deploy via GitOps
|
||||
run: |
|
||||
DOCS_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
bash ci/scripts/deploy-gitops.sh docs "${DOCS_TAG}"
|
||||
|
||||
verify-rollout:
|
||||
name: Verify - Production Rollout
|
||||
needs: deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
|
||||
steps:
|
||||
- name: Await production rollout
|
||||
run: |
|
||||
TARGET_COMMIT="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
ENDPOINT="https://docs.ermnvldmr.com/version.json"
|
||||
|
||||
echo "========================================================"
|
||||
echo "Verifying production deployment on $ENDPOINT"
|
||||
echo "Expected commit: $TARGET_COMMIT"
|
||||
echo "========================================================"
|
||||
|
||||
MAX_ATTEMPTS=30
|
||||
ATTEMPT=1
|
||||
|
||||
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
|
||||
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Polling $ENDPOINT..."
|
||||
|
||||
RESPONSE=$(curl -sf --max-time 10 "$ENDPOINT" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
LIVE_COMMIT=$(echo "$RESPONSE" | jq -r '.commit // empty')
|
||||
|
||||
echo " Live commit: ${LIVE_COMMIT:-<none>}"
|
||||
|
||||
if [ "$LIVE_COMMIT" = "$TARGET_COMMIT" ]; then
|
||||
echo "✅ Success! Production docs has successfully rolled out $TARGET_COMMIT."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo " Endpoint did not return valid JSON or timed out."
|
||||
fi
|
||||
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "❌ Deployment verification timed out after 5 minutes."
|
||||
exit 1
|
||||
host: ${{ secrets.DEPLOY_VPS_HOST }}
|
||||
username: ${{ secrets.DEPLOY_VPS_USER }}
|
||||
key: ${{ secrets.DEPLOY_VPS_SSH_KEY }}
|
||||
port: ${{ secrets.DEPLOY_VPS_SSH_PORT || 22 }}
|
||||
script: |
|
||||
cd /srv/ermnvldmr.com
|
||||
git pull
|
||||
chmod +x ci/scripts/deploy-new-tags.sh
|
||||
# Use the short SHA to match the tag from release-docker
|
||||
export DOCS_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
./ci/scripts/deploy-new-tags.sh
|
||||
@@ -11,10 +11,6 @@ concurrency:
|
||||
group: ci-pr-docs-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-hugo:
|
||||
name: Setup - Hugo
|
||||
|
||||
@@ -15,10 +15,6 @@ concurrency:
|
||||
group: ci-static-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -15,10 +15,6 @@ concurrency:
|
||||
group: ci-pr-static-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -2,20 +2,10 @@ name: services/www - next/checkout-release-branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
increment_major:
|
||||
description: 'Force major version increment (e.g. v2.0 instead of v1.1)'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
workflow-release-branch:
|
||||
name: Workflow - Create or Update Release Branch
|
||||
workflow-create-release-branch:
|
||||
name: Workflow - Create Release Branch
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -26,41 +16,19 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine Mode and Branch Name
|
||||
id: plan
|
||||
- name: Determine Branch Name
|
||||
id: branch
|
||||
run: |
|
||||
REF="${{ github.ref_name }}"
|
||||
if [[ "$REF" =~ ^releases/www/v[0-9]+\.[0-9]+$ ]]; then
|
||||
# Dispatched against an existing release branch: update it.
|
||||
echo "mode=update" >> "$GITHUB_OUTPUT"
|
||||
echo "branch_name=$REF" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Dispatched against next (or any other ref): create a new release branch.
|
||||
BRANCH_NAME=$(bash ci/scripts/semantic-release.sh checkout services/www "${{ inputs.increment_major }}")
|
||||
echo "mode=create" >> "$GITHUB_OUTPUT"
|
||||
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
BRANCH_NAME=$(bash ci/scripts/semantic-release.sh checkout services/www)
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and Push Branch
|
||||
if: steps.plan.outputs.mode == 'create'
|
||||
run: |
|
||||
git checkout -b ${{ steps.plan.outputs.branch_name }}
|
||||
git push origin ${{ steps.plan.outputs.branch_name }}
|
||||
|
||||
- name: Rebase Existing Branch onto next
|
||||
if: steps.plan.outputs.mode == 'update'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
# Bring the release branch up to date with its parent (next),
|
||||
# replaying any release-branch-only commits on top of the latest next.
|
||||
git checkout ${{ steps.plan.outputs.branch_name }}
|
||||
git fetch origin next
|
||||
git rebase origin/next
|
||||
git push --force-with-lease origin ${{ steps.plan.outputs.branch_name }}
|
||||
git checkout -b ${{ steps.branch.outputs.branch_name }}
|
||||
git push origin ${{ steps.branch.outputs.branch_name }}
|
||||
|
||||
- name: Trigger Release Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh workflow run services-www-releases-required.yml --ref ${{ steps.plan.outputs.branch_name }}
|
||||
gh workflow run services-www-releases-required.yml --ref ${{ steps.branch.outputs.branch_name }}
|
||||
@@ -15,10 +15,6 @@ concurrency:
|
||||
group: ci-www-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
@@ -357,10 +353,6 @@ jobs:
|
||||
type=raw,value=next
|
||||
type=sha,format=short
|
||||
|
||||
- name: Compute short SHA
|
||||
id: vars
|
||||
run: echo "short_sha=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -369,6 +361,3 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
COMMIT_SHA=${{ steps.vars.outputs.short_sha }}
|
||||
VERSION=${{ steps.vars.outputs.short_sha }}
|
||||
@@ -15,10 +15,6 @@ concurrency:
|
||||
group: ci-pr-www-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
@@ -2,21 +2,10 @@ name: services/www - releases/dispatch-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_ci:
|
||||
description: 'Skip CI checks (fast build, push & GitOps deploy)'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
|
||||
@@ -48,7 +37,6 @@ jobs:
|
||||
build-configs:
|
||||
name: Build - Configs
|
||||
needs: setup-stage
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -81,7 +69,6 @@ jobs:
|
||||
lint-www:
|
||||
name: Lint - WWW
|
||||
needs: build-configs
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -106,7 +93,6 @@ jobs:
|
||||
typecheck-www:
|
||||
name: Typecheck - WWW
|
||||
needs: build-configs
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -131,7 +117,6 @@ jobs:
|
||||
build-storybook:
|
||||
name: Build - Storybook
|
||||
needs: build-configs
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -162,7 +147,6 @@ jobs:
|
||||
test-www:
|
||||
name: Test - Unit
|
||||
needs: build-configs
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -187,7 +171,6 @@ jobs:
|
||||
test-storybook:
|
||||
name: Test - Storybook
|
||||
needs: build-storybook
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -219,7 +202,6 @@ jobs:
|
||||
build-www:
|
||||
name: Build - WWW Service
|
||||
needs: build-configs
|
||||
if: ${{ !inputs.skip_ci }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -244,14 +226,11 @@ jobs:
|
||||
release-tag-docker:
|
||||
name: Release - Tag & Docker
|
||||
needs: [lint-www, typecheck-www, test-www, test-storybook, build-www]
|
||||
if: ${{ always() && (inputs.skip_ci || (!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled'))) }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: services-www-releases-publish
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -265,11 +244,9 @@ jobs:
|
||||
# Extract MAJOR.MINOR for tagging
|
||||
MAJOR_MINOR=$(echo "${{ github.ref_name }}" | cut -d/ -f3 | sed 's/^v//')
|
||||
MAJOR=$(echo "$MAJOR_MINOR" | cut -d. -f1)
|
||||
SHORT_SHA="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
echo "version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "major_minor=$MAJOR_MINOR" >> $GITHUB_OUTPUT
|
||||
echo "major=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and Push Tag
|
||||
run: |
|
||||
@@ -304,79 +281,26 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
COMMIT_SHA=${{ steps.version.outputs.short_sha }}
|
||||
VERSION=v${{ steps.version.outputs.version }}
|
||||
|
||||
deploy:
|
||||
name: Deploy - Kubernetes (GitOps)
|
||||
name: Deploy - VPS
|
||||
needs: release-tag-docker
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: services-www-releases-deploy
|
||||
url: https://ermnvldmr.com
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
ref: next
|
||||
token: ${{ secrets.GITOPS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Kustomize
|
||||
uses: imranismail/setup-kustomize@v2
|
||||
with:
|
||||
kustomize-version: "5.3.0"
|
||||
|
||||
- name: Deploy via GitOps
|
||||
run: |
|
||||
WWW_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
bash ci/scripts/deploy-gitops.sh www "${WWW_TAG}"
|
||||
|
||||
verify-rollout:
|
||||
name: Verify - Production Rollout
|
||||
needs: [release-tag-docker, deploy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Await production rollout
|
||||
run: |
|
||||
TARGET_COMMIT="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
TARGET_VERSION="v${{ needs.release-tag-docker.outputs.version }}"
|
||||
ENDPOINT="https://www.ermnvldmr.com/version.json"
|
||||
|
||||
echo "========================================================"
|
||||
echo "Verifying production deployment on $ENDPOINT"
|
||||
echo "Expected commit: $TARGET_COMMIT"
|
||||
echo "Expected version: $TARGET_VERSION"
|
||||
echo "========================================================"
|
||||
|
||||
MAX_ATTEMPTS=30
|
||||
ATTEMPT=1
|
||||
|
||||
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
|
||||
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Polling $ENDPOINT..."
|
||||
|
||||
RESPONSE=$(curl -sf --max-time 10 "$ENDPOINT" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
LIVE_COMMIT=$(echo "$RESPONSE" | jq -r '.commit // empty')
|
||||
LIVE_VERSION=$(echo "$RESPONSE" | jq -r '.version // empty')
|
||||
|
||||
echo " Live commit: ${LIVE_COMMIT:-<none>}"
|
||||
echo " Live version: ${LIVE_VERSION:-<none>}"
|
||||
|
||||
if [ "$LIVE_COMMIT" = "$TARGET_COMMIT" ] || [ "$LIVE_VERSION" = "$TARGET_VERSION" ]; then
|
||||
echo "✅ Success! Production has successfully rolled out $TARGET_VERSION ($TARGET_COMMIT)."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo " Endpoint did not return valid JSON or timed out."
|
||||
fi
|
||||
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "❌ Deployment verification timed out after 5 minutes."
|
||||
exit 1
|
||||
host: ${{ secrets.DEPLOY_VPS_HOST }}
|
||||
username: ${{ secrets.DEPLOY_VPS_USER }}
|
||||
key: ${{ secrets.DEPLOY_VPS_SSH_KEY }}
|
||||
port: ${{ secrets.DEPLOY_VPS_SSH_PORT || 22 }}
|
||||
script: |
|
||||
cd /srv/ermnvldmr.com
|
||||
git pull
|
||||
chmod +x ci/scripts/deploy-new-tags.sh
|
||||
# Use the short SHA to match the tag from release-tag-docker
|
||||
export WWW_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
./ci/scripts/deploy-new-tags.sh
|
||||
@@ -9,10 +9,6 @@ concurrency:
|
||||
group: ci-www-rel-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
setup-stage:
|
||||
name: Setup - Stage
|
||||
|
||||
+2
-14
@@ -2,9 +2,6 @@
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# service static assets (local only, synced with S3)
|
||||
**/public/static/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
@@ -21,9 +18,9 @@ storybook-static/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
# environment variables (.env.dev is encrypted and tracked; .env is plaintext)
|
||||
# environment variables
|
||||
.env
|
||||
.env.keys
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
@@ -34,12 +31,3 @@ playwright-report/
|
||||
# agents plans and designs
|
||||
docs/plans/
|
||||
docs/superpowers/
|
||||
|
||||
# SOPS and Kubernetes Secrets
|
||||
*.dec.yaml
|
||||
*.plain.yaml
|
||||
*.local.yaml
|
||||
*.key
|
||||
.sops/keys.txt
|
||||
ci/k8s/**/secret.yaml
|
||||
ci/cloud-init/user-data.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
creation_rules:
|
||||
- path_regex: ci/k8s/.*secret.*\.yaml$
|
||||
encrypted_regex: '^(data|stringData)$'
|
||||
age: "age1jpa7hqyzzdfq7lvnw3l8qy6648mtjgpcqnljl5jz92dcqjrnsqfsn7m0fl"
|
||||
@@ -1,147 +1,444 @@
|
||||
# AGENTS.md
|
||||
# AGENTS.md - Development Guide
|
||||
|
||||
Guidance and operational constraints for AI coding agents working on `ermnvldmr.com`.
|
||||
## 📋 Project Overview
|
||||
|
||||
## 1. System Overview & Tech Stack
|
||||
**ermnvldmr.com** is Vladimir Eremin's personal ecosystem of services delivered through a modern monorepo architecture. The project encompasses personal websites, documentation sites, and shared infrastructure for web-based content and services.
|
||||
|
||||
`ermnvldmr.com` is a pnpm monorepo hosting personal web services, dividing applications into `services/` and shared packages into `packages/`. The primary website (`services/www`) and static pages (`services/static`) are built with React 19 and Rsbuild (Rspack). Build-time Static Site Generation is performed via `@ermnvldmr/ssg`, a custom Rsbuild plugin that pre-renders React entry points to HTML with inlined critical CSS (via Critters). The documentation service (`services/docs`) runs on Hugo with the Hextra theme.
|
||||
### Monorepo Architecture
|
||||
|
||||
Shared workspace packages provide the UI design system (`@ermnvldmr/ui`), application component kits (`@ermnvldmr/kits`), standard types library (`@ermnvldmr/stl`), localization utilities (`@ermnvldmr/i18n`), monorepo CLI tooling (`@ermnvldmr/cli`), static site generator (`@ermnvldmr/ssg`), and reusable presets for ESLint, Rsbuild, and Storybook. Design components utilize Tailwind CSS v4, Radix UI primitives, React-Aria, and CVA, validated using Vitest and Storybook 8.
|
||||
- **Repository Structure**: pnpm workspace monorepo
|
||||
- **Package Manager**: pnpm v9.12.3+
|
||||
- **Core Technology**: TypeScript (Strict mode)
|
||||
- **Services**: Independent web services with shared dependencies
|
||||
- **Packages**: Reusable configurations and component libraries
|
||||
- **Build System**: Distributed builds with dependency management
|
||||
- **CI/CD**: GitHub Actions with path-based triggers
|
||||
|
||||
| Technology | Version / Tool | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **Package Manager** | `pnpm` (>=9.12.3) | Workspace dependency and package management |
|
||||
| **Runtime Engine** | Node.js (>=20) | Server and build runtime environment |
|
||||
| **UI Library** | React 19 (`react`, `react-dom`) | Declarative UI rendering |
|
||||
| **Bundler Engine** | Rsbuild / Rspack | Rust-powered bundling, hot module reloading, and build pipeline |
|
||||
| **Static Site Generator** | `@ermnvldmr/ssg` / Hugo (>=0.110.0) | Build-time React HTML pre-rendering & Hugo docs engine |
|
||||
| **UI Primitives & Icons** | React Aria, Radix UI, Lucide React | Accessible interactive primitives and icon system |
|
||||
| **Styling & Utilities** | Tailwind CSS v4, CVA, `@ermnvldmr/stl` | Utility styling, component variant management, class utilities |
|
||||
| **Internationalization** | `@ermnvldmr/i18n` | Multi-locale routing and localization strings (`en`, `ru`) |
|
||||
| **Testing Framework** | Vitest, Testing Library, Playwright | Unit, integration, and Storybook interaction testing |
|
||||
| **Component Workshop** | Storybook 8 | Isolated component development environment |
|
||||
| **Static Analysis** | ESLint 9 (Flat Config), TypeScript 5.8 | Code linting, monorepo import boundary checks, strict typing |
|
||||
| **Secrets Encryption** | `@dotenvx/dotenvx` | Public-key encrypted `.env.dev` secrets management |
|
||||
### Design Philosophy
|
||||
|
||||
## 2. Project Structure & Key Directories
|
||||
- **Component-First**: All UI elements developed as isolated, reusable components
|
||||
- **Configuration Sharing**: Centralized tooling configurations across services
|
||||
- **TypeScript-First**: Strict typing for all public APIs and components
|
||||
- **Documentation-Driven**: TSDoc required for all exported elements
|
||||
- **Dependency Isolation**: Clear separation between packages and services
|
||||
- **Build Optimization**: Efficient dependency building and caching
|
||||
|
||||
```text
|
||||
ermnvldmr.com/
|
||||
├── ci/ # CI/CD pipelines, Docker containers, deployment scripts
|
||||
├── docs/ # Architectural documentation and design specifications
|
||||
├── packages/ # Monorepo shared packages and toolchain configurations
|
||||
│ ├── cli/ # Monorepo CLI utility (`ermnvldmr-cli`) for tasks like S3 asset sync
|
||||
│ ├── eslint-config/ # Shared flat ESLint rule sets and monorepo import restriction rules
|
||||
│ ├── i18n/ # Localization engine, locale pathing, and translation keysets
|
||||
│ ├── kits/ # High-level domain & layout component kits (BentoGrid, InfoCard, PageHead, Page, etc.)
|
||||
│ ├── rsbuild-config/ # Shared Rsbuild and Rspack bundler configuration presets
|
||||
│ ├── ssg/ # Build-time Static Site Generation plugin for Rsbuild React apps
|
||||
│ ├── stl/ # Standard Types Library for core utilities, type definitions, and react helpers
|
||||
│ ├── storybook-config/ # Shared Storybook workshop configurations
|
||||
│ └── ui/ # Design system React component library, styles, and Storybook stories
|
||||
├── services/ # Deployable web applications and services
|
||||
│ ├── docs/ # Technical documentation site (Hugo + Hextra theme)
|
||||
│ ├── static/ # Static landing pages service (Rsbuild + React 19)
|
||||
│ └── www/ # Primary website (Rsbuild + React 19 + MDX + SSG)
|
||||
├── .env.dev # Encrypted environment variable storage (tracked in git)
|
||||
├── .env.keys # Private decryption keys (UNTRACKED - do not commit)
|
||||
├── eslint.config.mjs # Root ESLint flat configuration extending `@ermnvldmr/eslint-config`
|
||||
├── pnpm-workspace.yaml # Monorepo workspace package definitions
|
||||
└── tsconfig.json # Root TypeScript configuration with workspace path aliases
|
||||
## 🏗️ Repository Structure
|
||||
|
||||
```
|
||||
├── packages/ # Shared packages and configurations
|
||||
│ ├── eslint-config/ # Shared ESLint configurations
|
||||
│ ├── rsbuild-config/ # Shared Rsbuild configurations
|
||||
│ ├── storybook-config/ # Shared Storybook configurations
|
||||
│ ├── stl/ # Standard Type Library (utilities)
|
||||
│ └── ui/ # Shared React component library
|
||||
├── services/ # Independent web services
|
||||
│ ├── www/ # Main website (Rsbuild + React)
|
||||
│ └── docs/ # Documentation site (Hugo)
|
||||
├── .github/workflows/ # CI/CD automation
|
||||
├── pnpm-workspace.yaml # Workspace configuration
|
||||
└── [root config files] # Global configurations
|
||||
```
|
||||
|
||||
## 3. Setup & Commands
|
||||
### Package Organization
|
||||
|
||||
### CLI Commands
|
||||
#### Configuration Packages (`packages/`)
|
||||
|
||||
- **Install dependencies**: `pnpm install`
|
||||
- **Development servers**:
|
||||
- Website (`services/www`): `pnpm www:dev` (or `pnpm dev:www`)
|
||||
- Documentation (`services/docs`): `pnpm docs:dev` (or `pnpm dev:docs`)
|
||||
- UI Component Storybook (`packages/ui`): `pnpm ui:storybook`
|
||||
- Static landings (`services/static`): `pnpm static:dev` (or `pnpm dev:static`)
|
||||
- UI package build watcher: `pnpm ui:dev`
|
||||
- **Build**:
|
||||
- Full workspace build: `pnpm build`
|
||||
- Workspace package dependencies only: `pnpm build:deps`
|
||||
- Individual package build: `pnpm --filter @ermnvldmr/ui build`
|
||||
- **Linting & Formatting**:
|
||||
- Lint workspace: `pnpm lint`
|
||||
- Typecheck workspace: `pnpm typecheck`
|
||||
- Format files: `pnpm format`
|
||||
- **Testing**:
|
||||
- Run full test suite: `pnpm test`
|
||||
- Test single package: `pnpm --filter @ermnvldmr/ui test`
|
||||
- Test single file: `pnpm --filter @ermnvldmr/ui exec vitest run src/components/Button/Button.test.tsx`
|
||||
- Test coverage: `pnpm test:coverage`
|
||||
- **eslint-config**: Shared ESLint rules for TypeScript and React
|
||||
- **rsbuild-config**: Shared Rsbuild and Rspack configurations for library and service builds
|
||||
- **storybook-config**: Shared Storybook setup and configurations
|
||||
|
||||
### Environment Setup
|
||||
#### Library Packages (`packages/`)
|
||||
|
||||
- Environment variables are managed using `@dotenvx/dotenvx`.
|
||||
- `.env.dev` contains encrypted values and is tracked in git.
|
||||
- `.env.keys` contains local decryption keys (`DOTENV_PRIVATE_KEY`) and must never be committed.
|
||||
- Environment variables:
|
||||
- `LOCALE`: Target build language (`en` or `ru`, defaults to `en`).
|
||||
- `RSDOCTOR`: Set `RSDOCTOR=true` during build to trigger Rsbuild bundle analysis.
|
||||
- S3 static sync credentials: `COM_ERMNVLDMR_STATIC_S3_BUCKET`, `COM_ERMNVLDMR_STATIC_S3_REGION`, `COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL`, `COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID`, `COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY`.
|
||||
- **stl** (Standard Type Library): Common utilities, types, and helper functions
|
||||
- **ui**: React component library with design system and Storybook stories
|
||||
- **i18n**: Internationalization utilities
|
||||
|
||||
## 4. Non-Negotiable Coding Rules & Patterns
|
||||
#### Service Packages (`services/`)
|
||||
|
||||
### Typing & Documentation Rules
|
||||
- **www**: Main website built with Rsbuild, React, and Tailwind CSS
|
||||
- **static**: Static landing pages service built with Rsbuild and React
|
||||
- **docs**: Documentation site built with Hugo and Hextra theme
|
||||
|
||||
- **Strict TypeScript**: `strict: true` is enforced globally.
|
||||
- **Forbidden Types**: `any` is strictly prohibited (`@typescript-eslint/no-explicit-any: error`).
|
||||
- **Forbidden Assertions**: Type assertions (`as Type`) are strictly prohibited (`@typescript-eslint/consistent-type-assertions: ['error', { assertionStyle: 'never' }]`). Use explicit type guards or type casting helpers (e.g., `castRef` from `@ermnvldmr/stl`).
|
||||
- **Forbidden Non-Null Assertion**: Non-null assertion operator (`!`) is prohibited (`@typescript-eslint/no-non-null-assertion: error`).
|
||||
- **Explicit Type Imports**: Imports used strictly for types MUST use `import type` (`@typescript-eslint/consistent-type-imports: error`).
|
||||
- **Interface Definitions**: Object shapes MUST use `interface`, not `type` aliases (`@typescript-eslint/consistent-type-definitions: ['error', 'interface']`).
|
||||
- **TSDoc Requirements**: JSDoc comments are required on all exported function declarations, interfaces, and type declarations (`jsdoc/require-jsdoc: error`). A description string is mandatory (`jsdoc/require-description: error`).
|
||||
### Key Directory Rules
|
||||
|
||||
### Component Architecture & Structure
|
||||
#### `/packages/`
|
||||
|
||||
- Component implementations in `packages/ui` MUST live in dedicated PascalCase subdirectories:
|
||||
`packages/ui/src/components/<ComponentName>/`
|
||||
- `<ComponentName>.tsx` — Component logic and JSX
|
||||
- `<ComponentName>.stories.tsx` — Storybook stories
|
||||
- `<ComponentName>.test.tsx` — Vitest unit tests
|
||||
- **Styling**: Component variant logic must use `cva` (class-variance-authority). Utility class merging must use `cn(...)` from `@ermnvldmr/stl`.
|
||||
- **Accessibility**: Interactive elements must leverage `react-aria` hooks (`useButton`, `useFocusRing`, `useHover`) to maintain accessibility standards.
|
||||
- **Naming Conventions**:
|
||||
- PascalCase for React component files and directories (`Button/Button.tsx`).
|
||||
- camelCase for helper modules, hooks, and utilities (`path.ts`, `useTheme.ts`).
|
||||
- kebab-case for configuration files (`eslint.config.mjs`, `postcss.config.mjs`).
|
||||
- **Purpose**: Shared configurations and reusable libraries
|
||||
- **Dependencies**: Can depend on each other following build order (configs → stl → ui)
|
||||
- **Exports**: Must provide proper TypeScript declarations
|
||||
- **Building**: Required before dependent services can build
|
||||
- **Testing**: Individual test suites for each package
|
||||
|
||||
### State Management & Import Rules
|
||||
#### `/services/`
|
||||
|
||||
- **State Primitives**: Use standard React state (`useState`, `useReducer`, `useContext`) and `react-stately` / `react-aria` primitives. Do not introduce unapproved third-party state managers.
|
||||
- **Monorepo Relative Imports**: Code inside a package or service MUST use relative paths when referencing files within that same package. Importing the package's own published name inside itself (e.g. `import ... from '@ermnvldmr/ui'` inside `packages/ui`) is strictly prohibited and fails ESLint checks.
|
||||
- **Purpose**: Independent web applications and sites
|
||||
- **Dependencies**: Can consume packages but not other services
|
||||
- **Deployment**: Independent deployment pipelines
|
||||
- **Development**: Can be developed and tested in isolation
|
||||
- **Configuration**: Inherit from shared packages but can override
|
||||
|
||||
### Anti-Patterns ("What NOT to do")
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
- **DO NOT** use `any`, `as Type` assertions, or `!` non-null assertions.
|
||||
- **DO NOT** introduce Vite, Webpack, or Next.js configurations. Rsbuild (Rspack) is the monorepo's standardized React bundler.
|
||||
- **DO NOT** use self-referential package imports within workspace packages; use relative paths.
|
||||
- **DO NOT** suppress linter or compiler errors with `@ts-ignore`, `@ts-nocheck`, or inline eslint disables without explicit user approval.
|
||||
- **DO NOT** commit unencrypted `.env` files or secret keys (`.env.keys`).
|
||||
- **DO NOT** run `git commit` or execute automated git commits under any circumstances.
|
||||
### Monorepo Development Principles
|
||||
|
||||
## 5. Critical Workflows & Safety Directives
|
||||
- **Root-Level Scripts**: Check `package.json` at repository root for monorepo-wide operations
|
||||
- **Service-Specific Scripts**: Each service in `services/` has its own development commands
|
||||
- **Package Filtering**: Use pnpm workspace filtering to target specific packages/services
|
||||
- **Dependency-Aware Building**: UI components must be built before dependent services
|
||||
|
||||
### Verification Workflow
|
||||
### Package Development Workflow
|
||||
|
||||
Before completing any task, run and pass every step in the following sequence:
|
||||
The monorepo follows a strict dependency hierarchy that must be respected:
|
||||
|
||||
1. `pnpm typecheck` — 0 TypeScript compiler errors.
|
||||
2. `pnpm lint` — 0 ESLint warnings (`--max-warnings 0`).
|
||||
3. `pnpm test` — All unit and component tests passing.
|
||||
4. `pnpm build` — Clean workspace compilation.
|
||||
#### 1. Configuration Packages First
|
||||
|
||||
### Protected Boundaries
|
||||
Configuration packages provide shared tooling setups and must be built before anything else. Look for packages ending in `-config` in the `packages/` directory.
|
||||
|
||||
The agent MUST NOT modify the following files or directories unless explicitly requested by the user:
|
||||
#### 2. Library Packages
|
||||
|
||||
- `.env.keys` (untracked decryption keys).
|
||||
- `pnpm-lock.yaml` (dependencies must be managed exclusively through `pnpm` CLI commands).
|
||||
- `LICENSE` file.
|
||||
- `ci/` deployment configurations, Dockerfiles, and GitHub Actions workflows.
|
||||
- `services/docs/content/` and `services/www/content/` creative and literary works.
|
||||
Library packages (`stl`, `ui`) provide reusable code and components. They depend on configuration packages and must be built in dependency order (check their `package.json` dependencies).
|
||||
|
||||
#### 3. Services
|
||||
|
||||
Services consume packages but are independent of each other. They can be developed and deployed separately. Check `services/` directory for available services and their individual `package.json` files for available scripts.
|
||||
|
||||
### Code Organization Principles
|
||||
|
||||
- **Dependency Order**: configs → stl → ui → services
|
||||
- **Single Responsibility**: Each package serves one clear purpose
|
||||
- **Explicit Dependencies**: All inter-package dependencies declared in package.json
|
||||
- **Build Isolation**: Each package can be built independently
|
||||
- **Configuration Inheritance**: Services inherit from shared configurations
|
||||
- **Package Entry Points**:
|
||||
- `.` (Main): Runtime code used in production (e.g., `import { ... } from '@ermnvldmr/stl'`).
|
||||
- `/testing`: Test-related utilities and mocks (e.g., `import { ... } from '@ermnvldmr/stl/testing'`).
|
||||
- `/dev`: Build-time dependencies (e.g., `import { ... } from '@ermnvldmr/stl/dev'`).
|
||||
- All other entry points are forbidden. Strictly build-time packages must still expose via `/dev`.
|
||||
- **Directory Structure (Packages & Services)**:
|
||||
- `src/lib/`: Helpers, utilities, and types.
|
||||
- `{category}/`: Grouped logic.
|
||||
- `index.ts`: Barrel export for all of `lib/`.
|
||||
- `testing.ts`: Testing utilities for the library.
|
||||
- `dev.ts`: Build-time utilities for the library.
|
||||
- `src/components/`: UI components.
|
||||
- `{ComponentName}/`: Isolated component directory.
|
||||
- `index.ts`: Barrel export for all components.
|
||||
- `testing.ts`: Component-specific testing utilities.
|
||||
- `src/static/`: Static assets (`.html`, `.css`, `.js`) directly in this directory (no subdirectories).
|
||||
- `src/app/` (Services only): Page definitions.
|
||||
- `{page}.tsx`: Single file per page, inlining the component in `createPage`.
|
||||
- `{path}/{page}.tsx`: Nested routing structure.
|
||||
- Top-level `src/index.ts`, `src/testing.ts`, and `src/dev.ts` should only re-export from `lib/` or `components/`.
|
||||
|
||||
## 📝 Coding Conventions
|
||||
|
||||
### Package Naming Convention
|
||||
|
||||
All packages use the `@ermnvldmr/` namespace:
|
||||
|
||||
- `@ermnvldmr/eslint-config` - Shared ESLint configurations
|
||||
- `@ermnvldmr/rsbuild-config` - Shared Rsbuild configurations
|
||||
- `@ermnvldmr/storybook-config` - Shared Storybook configurations
|
||||
- `@ermnvldmr/stl` - Standard Type Library
|
||||
- `@ermnvldmr/ui` - Component library
|
||||
- `@ermnvldmr/i18n` - Internationalization utilities
|
||||
- `@ermnvldmr/www` - Main website service
|
||||
- `@ermnvldmr/static` - Static landing pages service
|
||||
- `@ermnvldmr/docs` - Documentation service
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
- **Strict Mode**: All TypeScript strict checks must pass
|
||||
- **No 'any'**: The use of `any` is strictly prohibited. Use `unknown` if the type is truly unknown, or define proper generic constraints.
|
||||
- **No 'ts-ignore' or 'ts-expect-error'**: Never suppress compiler errors. If a third-party library has missing types, provide a local declaration file (.d.ts) instead.
|
||||
- **No Unsafe Assertions**: Avoid `as any` or `as T` assertions. Prefer type guards, `instanceof` checks, or proper schema validation (e.g., Zod) to narrow types.
|
||||
- **Explicit Types**: Avoid type inference for public APIs; prefer explicit type definitions for exports.
|
||||
- **Interface First**: Use interfaces for component props and public APIs.
|
||||
- **Cross-Package Types**: Export types from packages for service consumption.
|
||||
|
||||
### TSDoc Documentation Requirements
|
||||
|
||||
**MANDATORY for all exported elements**: types, functions, constants, components
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Combines class names using clsx and tailwind-merge for consistent styling.
|
||||
*
|
||||
* @param inputs - Class name inputs to combine
|
||||
* @returns Combined and deduplicated class string
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* cn('px-4 py-2', 'bg-blue-500', 'px-6') // 'py-2 bg-blue-500 px-6'
|
||||
* cn('text-lg', condition && 'font-bold') // 'text-lg font-bold'
|
||||
* \`\`\`
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
### Internationalization (i18n) Guidelines
|
||||
|
||||
- **Package**: Use the `@ermnvldmr/i18n` package for all translations across services.
|
||||
- **Keyset Pattern**: Create `.i18n.ts` files alongside components that require translation.
|
||||
- **Russian Translation Style**: Embrace the Russian language fully. Prefer phonetic transliteration/transcription for global brand names and descriptive terms over keeping them in English.
|
||||
- **Brands**: 'GitHub' → 'ГитХаб', 'Instagram' → 'Инстаграм', 'Telegram' → 'Телеграм'.
|
||||
- **Descriptive**: 'Email' → 'Электропочта'.
|
||||
- **Source of Truth**: The English locale (`en`) serves as the primary reference for keys and structure.
|
||||
|
||||
### Component Development Patterns
|
||||
|
||||
#### UI Package Components
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Props for the Button component.
|
||||
*/
|
||||
export interface ButtonProps {
|
||||
/** The button content */
|
||||
children: React.ReactNode;
|
||||
/** Visual style variant */
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
/** Button size */
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Versatile button component with multiple variants and sizes.
|
||||
*/
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- **Components**: PascalCase (e.g., `Button.tsx`, `NavigationMenu.tsx`)
|
||||
- **Utilities**: camelCase (e.g., `cn.ts`, `formatters.ts`)
|
||||
- **Constants**: UPPER_SNAKE_CASE for exports (e.g., `API_ENDPOINTS`)
|
||||
- **Types**: PascalCase with descriptive suffixes (e.g., `ButtonProps`, `ApiResponse`)
|
||||
- **Config Files**: kebab-case (e.g., `rsbuild-config`, `eslint-config`)
|
||||
|
||||
### Module Structure Patterns
|
||||
|
||||
#### Package Structure
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── .config/
|
||||
│ ├── vitest/
|
||||
│ │ └── setup.ts
|
||||
│ └── storybook/
|
||||
│ ├── main.ts
|
||||
│ └── preview.tsx
|
||||
├── src/
|
||||
│ ├── components/ # Component categories
|
||||
│ │ ├── index.ts # Production barrel
|
||||
│ │ ├── testing.ts # Testing barrel
|
||||
│ │ └── Button/ # Isolated component
|
||||
│ ├── lib/ # Core logic
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── testing.ts
|
||||
│ │ └── dev.ts
|
||||
│ ├── static/ # Flat static assets (CSS, etc.)
|
||||
│ ├── index.ts # Main entry point (runtime)
|
||||
│ ├── testing.ts # Testing entry point
|
||||
│ └── dev.ts # Dev entry point
|
||||
├── package.json
|
||||
├── rsbuild.config.ts
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
#### Service Structure (www example)
|
||||
|
||||
```
|
||||
services/www/
|
||||
├── .config/
|
||||
│ ├── vitest/
|
||||
│ │ └── setup.ts
|
||||
│ └── storybook/
|
||||
├── src/
|
||||
│ ├── app/ # Single file per page
|
||||
│ │ ├── index.tsx # Inlined createPage
|
||||
│ │ └── articles/
|
||||
│ ├── components/ # layouts, widgets
|
||||
│ ├── lib/ # core, shared
|
||||
│ ├── static/ # global.css, index.html
|
||||
│ ├── index.ts
|
||||
│ └── ...
|
||||
├── public/ # Direct static assets (images)
|
||||
├── rsbuild.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 🎨 Design System (UI Package)
|
||||
|
||||
### Component Organization
|
||||
|
||||
- **Generic Components**: Layout utilities (Stack, VStack, HStack)
|
||||
- **Themed Components**: Paper-themed variants with consistent styling
|
||||
- **Radix Integration**: Accessibility-first components using Radix UI
|
||||
- **Storybook Stories**: Every component has comprehensive documentation
|
||||
|
||||
### Styling Approach
|
||||
|
||||
- **Tailwind CSS v4+**: Utility-first CSS framework
|
||||
- **Class Variance Authority**: Type-safe variant generation
|
||||
- **Custom Design Tokens**: Consistent spacing, colors, typography
|
||||
- **Responsive Design**: Mobile-first responsive patterns
|
||||
|
||||
### Component Categories
|
||||
|
||||
```typescript
|
||||
// Generic Layout Components
|
||||
export { Stack } from './components/generic/Stack/Stack';
|
||||
export { VStack } from './components/generic/VStack/VStack';
|
||||
export { HStack } from './components/generic/HStack/HStack';
|
||||
|
||||
// Themed Components (Paper)
|
||||
export { Button } from './components/paper/PaperButton/Button';
|
||||
export { Separator } from './components/paper/PaperSeparator/Separator';
|
||||
```
|
||||
|
||||
## ⚙️ Configuration Management
|
||||
|
||||
### Shared Configurations
|
||||
|
||||
Each configuration package provides standardized setups:
|
||||
|
||||
#### ESLint Configuration
|
||||
|
||||
```javascript
|
||||
// Provides: base, react configurations
|
||||
import { baseConfig } from '@ermnvldmr/eslint-config/dev';
|
||||
```
|
||||
|
||||
#### Vitest Configuration
|
||||
|
||||
```typescript
|
||||
// Provides: base, react configurations from rsbuild-config
|
||||
import { baseVitestConfig, reactVitestConfig } from '@ermnvldmr/rsbuild-config/dev';
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
```
|
||||
|
||||
#### Rsbuild Configuration
|
||||
|
||||
```typescript
|
||||
// Provides: definePackageConfig, defineServiceConfig
|
||||
import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
|
||||
```
|
||||
|
||||
### Service Configuration Inheritance
|
||||
|
||||
Services inherit from shared configs but can override:
|
||||
|
||||
```typescript
|
||||
// In service package.json devDependencies
|
||||
"@ermnvldmr/eslint-config": "workspace:*"
|
||||
"@ermnvldmr/rsbuild-config": "workspace:*"
|
||||
```
|
||||
|
||||
## 🚀 Build & Deployment
|
||||
|
||||
### Build Dependencies
|
||||
|
||||
The monorepo must respect a strict build order due to package dependencies:
|
||||
|
||||
1. **Configuration packages** (eslint-config, rsbuild-config, etc.) - Foundation tooling
|
||||
2. **STL package** - Shared utilities and types
|
||||
3. **UI package** - Component library (depends on STL)
|
||||
4. **Services** - Applications that consume packages (www, docs)
|
||||
|
||||
### CI/CD Pipeline Structure
|
||||
|
||||
The repository uses path-based CI/CD triggers - each package and service has dedicated workflows in `.github/workflows/`. Examine the workflow files to understand:
|
||||
|
||||
- **Trigger Patterns**: Which file changes activate each pipeline
|
||||
- **Build Dependencies**: How packages are built in sequence
|
||||
- **Quality Gates**: Linting, type-checking, and testing requirements
|
||||
- **Deployment Targets**: Where each service is deployed
|
||||
|
||||
## 🔧 Development Environment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: v20+
|
||||
- **pnpm**: v9.12.3+
|
||||
- **TypeScript**: v5.8+
|
||||
- **Hugo**: v0.110.0+ (for docs service)
|
||||
|
||||
### Initial Setup
|
||||
|
||||
To start development, follow the dependency hierarchy:
|
||||
|
||||
1. **Install dependencies** using the root `package.json` scripts
|
||||
2. **Build configuration packages** first (check packages ending in `-config`)
|
||||
3. **Build library packages** in dependency order (STL before UI)
|
||||
4. **Start services** after dependencies are built
|
||||
|
||||
### Development Commands
|
||||
|
||||
The monorepo provides scripts at multiple levels:
|
||||
|
||||
- **Root Level**: Check root `package.json` for monorepo-wide operations
|
||||
- **Package Level**: Each package has its own build, test, and development scripts
|
||||
- **Service Level**: Services have development servers, build commands, and testing
|
||||
|
||||
Use pnpm workspace filtering (`--filter`) to target specific packages or services. Examine individual `package.json` files to discover available scripts for each component.
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Testing Framework Distribution
|
||||
|
||||
- **Shared Configuration**: Vitest configs in `@ermnvldmr/rsbuild-config`
|
||||
- **Package Testing**: Each package has its own test suite
|
||||
- **Service Testing**: Services test their specific functionality
|
||||
- **Component Testing**: UI package uses React Testing Library with Vitest
|
||||
- **Storybook Testing**: Visual regression and interaction testing
|
||||
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
// Package-level testing (packages/stl/src/functions/cn/cn.test.ts)
|
||||
describe('cn function', () => {
|
||||
it('combines class names correctly', () => {
|
||||
expect(cn('px-4', 'py-2')).toBe('px-4 py-2');
|
||||
});
|
||||
});
|
||||
|
||||
// Component testing (packages/ui/src/components/paper/Button/Button.test.tsx)
|
||||
describe('components/paper/Button/Button', () => {
|
||||
it('renders with correct variant', () => {
|
||||
render(<Button variant="destructive">Test</Button>);
|
||||
expect(screen.getByRole('button')).toHaveClass('destructive');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Coverage Strategy
|
||||
|
||||
- **Package Coverage**: Individual coverage reports per package
|
||||
- **Service Coverage**: Service-specific coverage including Storybook tests
|
||||
- **Aggregated Coverage**: Combined reporting for the entire monorepo
|
||||
|
||||
---
|
||||
|
||||
_This guide serves as the architectural foundation for the monorepo. Focus on these stable patterns and structures rather than specific implementation details that may change over time._
|
||||
|
||||
## 🤖 AI Agent Best Practices
|
||||
|
||||
The following are explicit best practices and preferences intended for AI tools and agents operating within this repository:
|
||||
|
||||
- **Build System Preference**: Rsbuild and Rspack are the standard and strongly preferred build tools for all projects in this monorepo due to their Rust-based architecture and performance. Do not introduce Vite, Astro, or Webpack unless strictly necessary.
|
||||
- **Framework Preference**: The monorepo has migrated away from Astro in favor of React + Rsbuild (with MDX support).
|
||||
- **Content Sites**: Rspress is the preferred infrastructure for any new content-heavy or documentation sites.
|
||||
@@ -29,12 +29,9 @@ Composite (see [Copyright & License](#copyright--license))
|
||||
|
||||
#### Packages
|
||||
|
||||
- **[@ermnvldmr/ui](./packages/ui)**: Shared React component library and design system primitives.
|
||||
- **[@ermnvldmr/kits](./packages/kits)**: High-level application & layout component kits (`BentoGrid`, `InfoCard`, `PageHead`, `Page`, `Time`, `Markdown`, `ThemeSwitch`).
|
||||
- **[@ermnvldmr/stl](./packages/stl)**: Standard Types Library – core utilities, type definitions, and React helpers.
|
||||
- **[@ermnvldmr/ui](./packages/ui)**: Shared React component library and design system.
|
||||
- **[@ermnvldmr/stl](./packages/stl)**: Standard Template Library – common utilities and reusable logic.
|
||||
- **[@ermnvldmr/i18n](./packages/i18n)**: Internationalization utilities.
|
||||
- **[@ermnvldmr/ssg](./packages/ssg)**: Build-time Static Site Generation plugin for Rsbuild.
|
||||
- **[@ermnvldmr/cli](./packages/cli)**: Monorepo CLI utility (`ermnvldmr-cli`).
|
||||
- **[@ermnvldmr/eslint-config](./packages/eslint-config)**: Shared linting configurations.
|
||||
- **[@ermnvldmr/rsbuild-config](./packages/rsbuild-config)**: Shared build configurations (Rsbuild & Rspack).
|
||||
- **[@ermnvldmr/storybook-config](./packages/storybook-config)**: Shared Storybook configurations.
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
#cloud-config
|
||||
# ==============================================================================
|
||||
# Cloud-Init User-Data Template for K3s Single-Node Cluster (com-ermnvldmr)
|
||||
#
|
||||
# Usage:
|
||||
# 1. Copy this template to user-data.yaml:
|
||||
# cp ci/cloud-init/user-data.template.yaml ci/cloud-init/user-data.yaml
|
||||
# 2. Replace the stubs below (SSH authorized key & Age private key).
|
||||
# 3. Pass user-data.yaml during VM creation in Yandex Cloud / Cloud Provider.
|
||||
# Note: user-data.yaml is ignored by git to protect private keys.
|
||||
# ==============================================================================
|
||||
|
||||
preserve_hostname: false
|
||||
hostname: com-ermnvldmr
|
||||
ssh_pwauth: false
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 1. User & SSH Key Configuration (Replace stub with your public SSH key)
|
||||
# ------------------------------------------------------------------------------
|
||||
users:
|
||||
- name: adam
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
ssh_authorized_keys:
|
||||
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLACEHOLDERFORYOURPUBLICSSHKEY user@hostname"
|
||||
|
||||
write_files:
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2. SOPS / Age Decryption Key (Replace stub with your private AGE key)
|
||||
# ----------------------------------------------------------------------------
|
||||
- path: /root/.config/sops/age/keys.txt
|
||||
owner: root:root
|
||||
permissions: '0600'
|
||||
content: |
|
||||
# public key: age1jpa7hqyzzdfq7lvnw3l8qy6648mtjgpcqnljl5jz92dcqjrnsqfsn7m0fl
|
||||
AGE-SECRET-KEY-1PLACEHOLDERFORYOURPRIVATEAGEKEY
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3. Security Hardening Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
- path: /etc/ssh/sshd_config.d/99-security-hardening.conf
|
||||
owner: root:root
|
||||
permissions: '0600'
|
||||
content: |
|
||||
PasswordAuthentication no
|
||||
PermitRootLogin prohibit-password
|
||||
KbdInteractiveAuthentication no
|
||||
X11Forwarding no
|
||||
MaxAuthTries 3
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
|
||||
- path: /etc/sysctl.d/99-k3s-hardening.conf
|
||||
owner: root:root
|
||||
permissions: '0644'
|
||||
content: |
|
||||
# Network SYN Flood Protection
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
# Disable ICMP redirects
|
||||
net.ipv4.conf.all.accept_redirects = 0
|
||||
net.ipv4.conf.default.accept_redirects = 0
|
||||
net.ipv4.conf.all.send_redirects = 0
|
||||
net.ipv4.conf.default.send_redirects = 0
|
||||
# Disable Source Routing
|
||||
net.ipv4.conf.all.accept_source_route = 0
|
||||
net.ipv4.conf.default.accept_source_route = 0
|
||||
# Anti-spoofing (Reverse Path Filtering)
|
||||
net.ipv4.conf.all.rp_filter = 1
|
||||
net.ipv4.conf.default.rp_filter = 1
|
||||
# Required for K8s pod networking
|
||||
net.ipv4.ip_forward = 1
|
||||
# System Hardening
|
||||
kernel.kptr_restrict = 2
|
||||
fs.protected_hardlinks = 1
|
||||
fs.protected_symlinks = 1
|
||||
fs.inotify.max_user_watches = 524288
|
||||
fs.inotify.max_user_instances = 512
|
||||
|
||||
- path: /etc/rancher/k3s/config.yaml
|
||||
owner: root:root
|
||||
permissions: '0600'
|
||||
content: |
|
||||
write-kubeconfig-mode: "0644"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 4. Bootstrap Cluster Script
|
||||
# ----------------------------------------------------------------------------
|
||||
- path: /usr/local/bin/bootstrap-cluster.sh
|
||||
owner: root:root
|
||||
permissions: '0755'
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== [Bootstrap] Ensuring Reliable DNS Resolution ==="
|
||||
if ! grep -q "nameserver" /etc/resolv.conf 2>/dev/null; then
|
||||
cat << 'EOF_DNS' > /etc/resolv.conf
|
||||
nameserver 1.1.1.1
|
||||
nameserver 8.8.8.8
|
||||
nameserver 77.88.8.8
|
||||
EOF_DNS
|
||||
fi
|
||||
|
||||
echo "=== [Bootstrap] Applying Sysctl Hardening ==="
|
||||
sysctl --system
|
||||
|
||||
echo "=== [Bootstrap] Ensuring SSH Host Keys & Starting SSH ==="
|
||||
ssh-keygen -A
|
||||
systemctl restart ssh || systemctl restart sshd || true
|
||||
|
||||
echo "=== [Bootstrap] Configuring UFW Firewall ==="
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw default allow routed
|
||||
ufw allow 22/tcp comment 'SSH'
|
||||
ufw allow 80/tcp comment 'HTTP Traefik'
|
||||
ufw allow 443/tcp comment 'HTTPS Traefik'
|
||||
# Allow Kubernetes Pod & Service traffic
|
||||
ufw allow in on cni0 || true
|
||||
ufw allow in on flannel.1 || true
|
||||
ufw allow from 10.42.0.0/16 comment 'K3s Pod CIDR'
|
||||
ufw allow from 10.43.0.0/16 comment 'K3s Service CIDR'
|
||||
ufw --force enable
|
||||
|
||||
echo "=== [Bootstrap] Enabling Fail2ban ==="
|
||||
systemctl enable --now fail2ban || true
|
||||
|
||||
echo "=== [Bootstrap] Checking for Secondary Persistent Data Disk ==="
|
||||
if [ -b /dev/vdb ]; then
|
||||
echo "Secondary disk /dev/vdb detected."
|
||||
if ! blkid /dev/vdb >/dev/null 2>&1; then
|
||||
echo "Formatting /dev/vdb with ext4 filesystem..."
|
||||
mkfs.ext4 -F -L k3s-storage /dev/vdb
|
||||
fi
|
||||
mkdir -p /var/lib/rancher/k3s/storage
|
||||
if ! grep -qs '/var/lib/rancher/k3s/storage' /proc/mounts; then
|
||||
mount /dev/vdb /var/lib/rancher/k3s/storage
|
||||
echo "LABEL=k3s-storage /var/lib/rancher/k3s/storage ext4 defaults,noatime 0 2" >> /etc/fstab
|
||||
echo "Mounted /dev/vdb to /var/lib/rancher/k3s/storage."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== [Bootstrap] Starting K3s Service ==="
|
||||
systemctl enable --now k3s.service
|
||||
|
||||
echo "=== [Bootstrap] Waiting for K3s API server ready ==="
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
until kubectl get nodes >/dev/null 2>&1; do
|
||||
echo "Waiting for Kubernetes API..."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
echo "=== [Bootstrap] Cluster node is online ==="
|
||||
kubectl get nodes
|
||||
|
||||
echo "=== [Bootstrap] Creating flux-system namespace ==="
|
||||
kubectl create namespace flux-system --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "=== [Bootstrap] Injecting SOPS age decryption key ==="
|
||||
if [ -f /root/.config/sops/age/keys.txt ]; then
|
||||
kubectl create secret generic sops-age \
|
||||
--namespace=flux-system \
|
||||
--from-file=age.agekey=/root/.config/sops/age/keys.txt \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
echo "sops-age secret created."
|
||||
else
|
||||
echo "WARNING: /root/.config/sops/age/keys.txt not found. Inject key manually via kubectl."
|
||||
fi
|
||||
|
||||
echo "=== [Bootstrap] Installing Flux Operator / Custom Resources ==="
|
||||
kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml
|
||||
|
||||
echo "=== [Bootstrap] Setting up Kubectl access for non-root users ==="
|
||||
for home_dir in /home/*; do
|
||||
if [ -d "$home_dir" ]; then
|
||||
user_name=$(basename "$home_dir")
|
||||
mkdir -p "$home_dir/.kube"
|
||||
cp /etc/rancher/k3s/k3s.yaml "$home_dir/.kube/config"
|
||||
chown -R "$user_name:$user_name" "$home_dir/.kube"
|
||||
chmod 600 "$home_dir/.kube/config"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== [Bootstrap] Applying initial GitOps root sync ==="
|
||||
kubectl apply -f - << 'EOF_GITOPS'
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: ermnvldmr-repo
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m
|
||||
url: https://github.com/deytenit/ermnvldmr.com.git
|
||||
ref:
|
||||
branch: next
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: cluster-sync
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: ermnvldmr-repo
|
||||
path: ./ci/k8s/clusters/production
|
||||
prune: true
|
||||
wait: true
|
||||
timeout: 2m
|
||||
decryption:
|
||||
provider: sops
|
||||
secretRef:
|
||||
name: sops-age
|
||||
EOF_GITOPS
|
||||
|
||||
echo "=== [Bootstrap] First-Boot Bootstrap Sequence Complete ==="
|
||||
|
||||
runcmd:
|
||||
- /usr/local/bin/bootstrap-cluster.sh
|
||||
@@ -44,13 +44,6 @@ server {
|
||||
try_files $uri $uri/ $uri.html /ru/index.html;
|
||||
}
|
||||
|
||||
# Version probe for release rollout verification
|
||||
location = /version.json {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||
add_header Content-Type "application/json" always;
|
||||
try_files /version.json =404;
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 404 /en/index.html;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Docker Image Tags
|
||||
DOCS_TAG=$DOCS_TAG
|
||||
WWW_TAG=$WWW_TAG
|
||||
|
||||
# Application Settings
|
||||
DOMAIN=ermnvldmr.com
|
||||
@@ -0,0 +1,61 @@
|
||||
name: com-ermnvldmr
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.28-alpine
|
||||
container_name: com-ermnvldmr-nginx
|
||||
restart: always
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
networks:
|
||||
- com-ermnvldmr-direct
|
||||
- com-ermnvldmr-enclave
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.50'
|
||||
memory: 256M
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
docs:
|
||||
image: ghcr.io/deytenit/ermnvldmr.com/docs:${DOCS_TAG}
|
||||
container_name: com-ermnvldmr-docs
|
||||
restart: always
|
||||
networks:
|
||||
- com-ermnvldmr-enclave
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.25'
|
||||
memory: 128M
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
www:
|
||||
image: ghcr.io/deytenit/ermnvldmr.com/www:${WWW_TAG}
|
||||
container_name: com-ermnvldmr-www
|
||||
restart: always
|
||||
networks:
|
||||
- com-ermnvldmr-enclave
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.25'
|
||||
memory: 128M
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
networks:
|
||||
com-ermnvldmr-direct:
|
||||
name: com-ermnvldmr-direct
|
||||
driver: bridge
|
||||
com-ermnvldmr-enclave:
|
||||
name: com-ermnvldmr-enclave
|
||||
internal: true
|
||||
driver: bridge
|
||||
@@ -0,0 +1,41 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name docs.ermnvldmr.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name docs.ermnvldmr.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/docs.ermnvldmr.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/docs.ermnvldmr.com/privkey.pem;
|
||||
|
||||
# Optimized SSL settings
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://com-ermnvldmr-docs:80;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.ermnvldmr.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name www.ermnvldmr.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/www.ermnvldmr.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/www.ermnvldmr.com/privkey.pem;
|
||||
|
||||
# Optimized SSL settings
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://com-ermnvldmr-www:80;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Optimization
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# Security Headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -14,15 +14,8 @@ RUN hugo --gc --minify
|
||||
# Stage 2: Runtime
|
||||
FROM nginx:alpine
|
||||
|
||||
ARG COMMIT_SHA="unknown"
|
||||
ARG VERSION="latest"
|
||||
|
||||
COPY --from=builder /src/services/docs/public /usr/share/nginx/html
|
||||
COPY ci/config/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Generate version metadata for automated rollout verification
|
||||
RUN printf '{\n "service": "docs",\n "version": "%s",\n "commit": "%s",\n "builtAt": "%s"\n}\n' \
|
||||
"${VERSION}" "${COMMIT_SHA}" "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > /usr/share/nginx/html/version.json
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -13,12 +13,9 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
# Copy all package.json files to allow pnpm to fetch dependencies
|
||||
COPY packages/eslint-config/package.json ./packages/eslint-config/
|
||||
COPY packages/rsbuild-config/package.json ./packages/rsbuild-config/
|
||||
COPY packages/ssg/package.json ./packages/ssg/
|
||||
COPY packages/stl/package.json ./packages/stl/
|
||||
COPY packages/storybook-config/package.json ./packages/storybook-config/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
COPY packages/kits/package.json ./packages/kits/
|
||||
COPY packages/cli/package.json ./packages/cli/
|
||||
COPY packages/i18n/package.json ./packages/i18n/
|
||||
COPY services/www/package.json ./services/www/
|
||||
|
||||
@@ -29,7 +26,10 @@ RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
# Build dependencies
|
||||
RUN pnpm build:deps
|
||||
RUN pnpm --filter @ermnvldmr/rsbuild-config build
|
||||
RUN pnpm --filter @ermnvldmr/stl build
|
||||
RUN pnpm --filter @ermnvldmr/i18n build
|
||||
RUN pnpm --filter @ermnvldmr/ui build
|
||||
|
||||
# Build the website (all locales)
|
||||
RUN pnpm --filter @ermnvldmr/www build:all
|
||||
@@ -37,15 +37,8 @@ RUN pnpm --filter @ermnvldmr/www build:all
|
||||
# Stage 2: Runtime
|
||||
FROM nginx:alpine
|
||||
|
||||
ARG COMMIT_SHA="unknown"
|
||||
ARG VERSION="latest"
|
||||
|
||||
COPY --from=builder /app/services/www/dist /usr/share/nginx/html
|
||||
COPY ci/config/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Generate version metadata for automated rollout verification
|
||||
RUN printf '{\n "service": "www",\n "version": "%s",\n "commit": "%s",\n "builtAt": "%s"\n}\n' \
|
||||
"${VERSION}" "${COMMIT_SHA}" "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > /usr/share/nginx/html/version.json
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,48 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docs
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docs
|
||||
spec:
|
||||
containers:
|
||||
- name: docs
|
||||
image: ghcr.io/deytenit/ermnvldmr.com/docs:sha-91f5245
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /en/
|
||||
port: 80
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /en/
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 2
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: com-ermnvldmr
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
images:
|
||||
- name: ghcr.io/deytenit/ermnvldmr.com/docs
|
||||
newTag: sha-ea805c7
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docs-service
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: docs
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-config
|
||||
namespace: com-ermnvldmr
|
||||
data:
|
||||
postgresql.conf: |
|
||||
# Low-memory tuned configuration for single-node K3s VM
|
||||
listen_addresses = '*'
|
||||
max_connections = 50
|
||||
shared_buffers = 64MB
|
||||
effective_cache_size = 128MB
|
||||
maintenance_work_mem = 16MB
|
||||
checkpoint_completion_target = 0.9
|
||||
wal_buffers = 2MB
|
||||
default_statistics_target = 100
|
||||
random_page_cost = 1.1
|
||||
effective_io_concurrency = 200
|
||||
work_mem = 4MB
|
||||
min_wal_size = 80MB
|
||||
max_wal_size = 512MB
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: com-ermnvldmr
|
||||
resources:
|
||||
- configmap.yaml
|
||||
- secret.sops.yaml
|
||||
- pvc.yaml
|
||||
- statefulset.yaml
|
||||
- service.yaml
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
@@ -1,25 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secrets
|
||||
namespace: com-ermnvldmr
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_DB: ENC[AES256_GCM,data:ChvCjVw=,iv:97n6Q+WzbVOwvRZGGHAfBXQDPx5mnUCSsUNs76xV1p4=,tag:mZLUB3q0UR+9ovqmndFrRg==,type:str]
|
||||
POSTGRES_USER: ENC[AES256_GCM,data:V1q4xfE=,iv:RPHHhWLkdmKHG6CMubrw8JUfHlDXnL0x76JEo05O+mo=,tag:PV1YEyqeb8bTF/98SL1uKQ==,type:str]
|
||||
POSTGRES_PASSWORD: ENC[AES256_GCM,data:aBWfOuPyeAEo0vf8fAYzlTfXooPKX6FPbHkpBQ+4zve0KBQh7/yvjQ==,iv:FB/fZoLirzPTbDIel7wRECQP85sFFlIdyoljrilbMxU=,tag:tTZz1PHhaLtmPO9Pg8Vh5g==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxRXIvMEpaWU1XNjV2OFpp
|
||||
Z1lYb25kOFVVaXlRYkdzSmxWbmxqaXR6TEhRCnF0M2ovRVNmdTU3WnNOcy82OExV
|
||||
YXNjZVJrbTdXQWJxMjQvNEdZNlBwcjAKLS0tIEEzczNmZjFwT1d2aXVKV1Bvdm1Q
|
||||
UnZoNVNCYjJiYWd6WlNobGEyUGh5NmMKkUTDkYlNkXH8DegvBT7ymkL7h1B9x77N
|
||||
IxeZI5mp8vc347/620+cbMMr2+aELlgs9/bJhUcLRm0Bv8UDVLOleg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jpa7hqyzzdfq7lvnw3l8qy6648mtjgpcqnljl5jz92dcqjrnsqfsn7m0fl
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-23T13:25:31Z"
|
||||
mac: ENC[AES256_GCM,data:tvk/U3XXcRrJCI8I0yUID6jwnlPc1ftiOHs8uxqrbAtLN3KelHGy94fWyxYMoboHeh5R8pNErIxk9EsJok2wjrSENZWMXueVZHtMUWlyC4DhTDAVVYhJ6sL85f6cMiHWDNSlTk4AvfWvdvE6b/0coZ7Rybgw5kNeW6Stw4CSbu4=,iv:sfiOamzam1e0t01KaGHSsnZOBpBGEi3K0KK50QDmaFs=,tag:Ul6xShhIdvTuX3qS6Uo4Kg==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secrets
|
||||
namespace: com-ermnvldmr
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_DB: "umami"
|
||||
POSTGRES_USER: "umami"
|
||||
POSTGRES_PASSWORD: "CHANGE_ME_IN_ENCRYPTED_SOPS_SECRET"
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-service
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
@@ -1,83 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
serviceName: postgres-service
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secrets
|
||||
key: POSTGRES_DB
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secrets
|
||||
key: POSTGRES_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secrets
|
||||
key: POSTGRES_PASSWORD
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: config
|
||||
mountPath: /etc/postgresql/postgresql.conf
|
||||
subPath: postgresql.conf
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- $(POSTGRES_USER)
|
||||
- -d
|
||||
- $(POSTGRES_DB)
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- $(POSTGRES_USER)
|
||||
- -d
|
||||
- $(POSTGRES_DB)
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
- name: config
|
||||
configMap:
|
||||
name: postgres-config
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: umami-config
|
||||
namespace: com-ermnvldmr
|
||||
data:
|
||||
DATABASE_TYPE: "postgresql"
|
||||
CLIENT_IP_HEADER: "X-Forwarded-For"
|
||||
@@ -1,53 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: umami
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: umami
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: umami
|
||||
spec:
|
||||
containers:
|
||||
- name: umami
|
||||
image: ghcr.io/umami-software/umami:postgresql-v2.14.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: umami-config
|
||||
- secretRef:
|
||||
name: umami-secrets
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/heartbeat
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/heartbeat
|
||||
port: 3000
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: com-ermnvldmr
|
||||
resources:
|
||||
- configmap.yaml
|
||||
- secret.sops.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
images:
|
||||
- name: ghcr.io/umami-software/umami
|
||||
newTag: postgresql-v2.14.0
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: umami-secrets
|
||||
namespace: com-ermnvldmr
|
||||
type: Opaque
|
||||
stringData:
|
||||
APP_SECRET: ENC[AES256_GCM,data:aaoXs+F/lXvP0kHNZ+Uoo0HB4GnYZ9raYOgEbRwIrkSFGSoTuv/PkvSOYDppDLKxhNMDLhbgTJwEMrHGZzIewQ==,iv:W3u4QPKetqBDSZdsRte5zYeFy9SlRmPw4ORGFTGzN7A=,tag:bkVyGsKgcMGODY4Chpv+8Q==,type:str]
|
||||
DATABASE_URL: ENC[AES256_GCM,data:XbAdd3hNGS+n42XgTQF5rg7AiBkbjFGHOeuTROLgjmIMCUh7afPSheFn6v8bSaibePYJdQBrdFK+kYW9o7aFYhe8Hz1cneATNP0PoQaqlxSrcnM+DeHQzFTC+98ACkskLgk27heZFjCdRr3A4LVRdcet/+2q/8U=,iv:UwxEwPeYdPYrvi30qso4yCZ6m20geusN+P7DOjowjEw=,tag:bQB7OupyNnCjn0INB8T7sg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGZ2h6THNGTnY2ay9Tc2Q3
|
||||
S0xxU3NEZGx2TS9MRFdqZDV6aEtUalBWRFVJCjZMd1VCdkg1cjIxSlliZXNRQnU0
|
||||
YnMzTXJOSGdsUDUwNkdDdWZWcmRaZjgKLS0tIDdEOFoyOWtvZytITys4bk9ra082
|
||||
WmFIREJMS1BVU0o1VnZHby9ZSHN4cGcKReBW7hdTPH8eGqPzEYkOdrIUYm9a1jOi
|
||||
du/1fhAyhjimbbCNdOSuSjrSP8QX6mxwPoDb1dVlKi0Rah5VGW+HWw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jpa7hqyzzdfq7lvnw3l8qy6648mtjgpcqnljl5jz92dcqjrnsqfsn7m0fl
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-23T13:25:31Z"
|
||||
mac: ENC[AES256_GCM,data:QZOb7pmfDpEn2/kefQQ8k4WJQddlF5jKTY5EaSm1Srt33DceELA3NPJMiozAzpbIEoLR6a7oR6C7zofHHVCBmYcRd2DSHqafW3EOtOt5noiAmkGOQKEWH7aSRFJqcy5EmBcVihd81Z1CkJI6MPcBeQvY0BlVqgm2YqX2QQBolgI=,iv:TB+Rn3ffe4HzVHjk2+XaKaLG1+589AmBhPjOXXKWHto=,tag:zfKVYP1h/4sWXSVtuhQH8Q==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: umami-secrets
|
||||
namespace: com-ermnvldmr
|
||||
type: Opaque
|
||||
stringData:
|
||||
APP_SECRET: "CHANGE_ME_IN_ENCRYPTED_SOPS_SECRET"
|
||||
DATABASE_URL: "postgresql://umami:CHANGE_ME@postgres-service.com-ermnvldmr.svc.cluster.local:5432/umami"
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: umami-service
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: umami
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
@@ -1,48 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: www
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: www
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: www
|
||||
spec:
|
||||
containers:
|
||||
- name: www
|
||||
image: ghcr.io/deytenit/ermnvldmr.com/www:sha-6d33b71
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /en/
|
||||
port: 80
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /en/
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 2
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: com-ermnvldmr
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
images:
|
||||
- name: ghcr.io/deytenit/ermnvldmr.com/www
|
||||
newTag: sha-6d33b71
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: www-service
|
||||
namespace: com-ermnvldmr
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: www
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: com-ermnvldmr
|
||||
labels:
|
||||
name: com-ermnvldmr
|
||||
@@ -1,29 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: ermnvldmr-repo
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m
|
||||
url: https://github.com/deytenit/ermnvldmr.com.git
|
||||
ref:
|
||||
branch: next
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: cluster-sync
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: ermnvldmr-repo
|
||||
path: ./ci/k8s/clusters/production
|
||||
prune: true
|
||||
wait: true
|
||||
timeout: 3m
|
||||
decryption:
|
||||
provider: sops
|
||||
secretRef:
|
||||
name: sops-age
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- gotk-sync.yaml
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- flux-system
|
||||
- ../../base/namespaces
|
||||
- ../../infrastructure/cert-manager
|
||||
- ../../infrastructure/networking
|
||||
- ../../apps/postgres
|
||||
- ../../apps/umami
|
||||
- ../../apps/www
|
||||
- ../../apps/docs
|
||||
@@ -1,29 +0,0 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: admin@ermnvldmr.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
email: admin@ermnvldmr.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml
|
||||
- cluster-issuer.yaml
|
||||
@@ -1,51 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: main-ingress
|
||||
namespace: com-ermnvldmr
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- www.ermnvldmr.com
|
||||
secretName: ermnvldmr-tls
|
||||
- hosts:
|
||||
- docs.ermnvldmr.com
|
||||
secretName: docs-ermnvldmr-tls
|
||||
- hosts:
|
||||
- umami.ermnvldmr.com
|
||||
secretName: umami-ermnvldmr-tls
|
||||
rules:
|
||||
- host: www.ermnvldmr.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: www-service
|
||||
port:
|
||||
number: 80
|
||||
- host: docs.ermnvldmr.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: docs-service
|
||||
port:
|
||||
number: 80
|
||||
- host: umami.ermnvldmr.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: umami-service
|
||||
port:
|
||||
number: 3000
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ingress.yaml
|
||||
@@ -1,2 +0,0 @@
|
||||
instance-id: debian-packer
|
||||
local-hostname: debian-k3s
|
||||
@@ -1,14 +0,0 @@
|
||||
#cloud-config
|
||||
ssh_pwauth: true
|
||||
disable_root: false
|
||||
chpasswd:
|
||||
list: |
|
||||
debian:packer
|
||||
root:packer
|
||||
expire: false
|
||||
users:
|
||||
- default
|
||||
- name: debian
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
lock_passwd: false
|
||||
@@ -1,74 +0,0 @@
|
||||
packer {
|
||||
required_plugins {
|
||||
qemu = {
|
||||
version = ">= 1.1.0"
|
||||
source = "github.com/hashicorp/qemu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "debian_cloud_image_url" {
|
||||
type = string
|
||||
default = "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.qcow2"
|
||||
}
|
||||
|
||||
variable "debian_cloud_image_checksum" {
|
||||
type = string
|
||||
default = "file:https://cloud.debian.org/images/cloud/bookworm/latest/SHA512SUMS"
|
||||
}
|
||||
|
||||
variable "vm_name" {
|
||||
type = string
|
||||
default = "com-ermnvldmr-debian-12-k3s-amd64.qcow2"
|
||||
}
|
||||
|
||||
variable "accelerator" {
|
||||
type = string
|
||||
default = "kvm"
|
||||
}
|
||||
|
||||
source "qemu" "debian_k3s" {
|
||||
iso_url = var.debian_cloud_image_url
|
||||
iso_checksum = var.debian_cloud_image_checksum
|
||||
disk_image = true
|
||||
disk_size = "10G"
|
||||
format = "qcow2"
|
||||
output_directory = "output-qemu"
|
||||
shutdown_command = "echo 'packer' | sudo -S shutdown -P now"
|
||||
accelerator = var.accelerator
|
||||
cd_files = ["ci/packer/cidata/user-data", "ci/packer/cidata/meta-data"]
|
||||
cd_label = "cidata"
|
||||
ssh_username = "debian"
|
||||
ssh_password = "packer"
|
||||
ssh_timeout = "5m"
|
||||
vm_name = var.vm_name
|
||||
net_device = "virtio-net"
|
||||
disk_interface = "virtio"
|
||||
headless = true
|
||||
|
||||
qemuargs = [
|
||||
["-m", "2048M"],
|
||||
["-smp", "2"]
|
||||
]
|
||||
}
|
||||
|
||||
build {
|
||||
sources = ["source.qemu.debian_k3s"]
|
||||
|
||||
provisioner "shell" {
|
||||
execute_command = "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
|
||||
scripts = [
|
||||
"ci/packer/scripts/01-base-system.sh",
|
||||
"ci/packer/scripts/02-install-k3s.sh",
|
||||
"ci/packer/scripts/03-cleanup.sh"
|
||||
]
|
||||
}
|
||||
|
||||
post-processor "shell-local" {
|
||||
inline = [
|
||||
"echo 'Optimizing qcow2 image with 2M cluster size...'",
|
||||
"qemu-img convert -f qcow2 -O qcow2 -o cluster_size=2M -c output-qemu/${var.vm_name} output-qemu/debian-12-k3s-optimized.qcow2",
|
||||
"mv output-qemu/debian-12-k3s-optimized.qcow2 output-qemu/${var.vm_name}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
d-i debian-installer/locale string en_US.UTF-8
|
||||
d-i keyboard-configuration/xkb-keymap select us
|
||||
|
||||
d-i netcfg/choose_interface select auto
|
||||
d-i netcfg/get_hostname string debian-k3s
|
||||
d-i netcfg/get_domain string local
|
||||
|
||||
d-i mirror/country string manual
|
||||
d-i mirror/http/hostname string deb.debian.org
|
||||
d-i mirror/http/directory string /debian
|
||||
d-i mirror/http/proxy string
|
||||
|
||||
d-i passwd/root-login boolean true
|
||||
d-i passwd/root-password password packer
|
||||
d-i passwd/root-password-again password packer
|
||||
d-i passwd/make-user boolean false
|
||||
|
||||
d-i clock-setup/utc boolean true
|
||||
d-i time/zone string UTC
|
||||
|
||||
d-i partman-auto/method string regular
|
||||
d-i partman-auto/disk string /dev/vda
|
||||
d-i partman-auto/choose_recipe select gpt-boot-root
|
||||
d-i partman-auto/expert_recipe string \
|
||||
gpt-boot-root :: \
|
||||
512 512 512 fat32 \
|
||||
$primary{ } \
|
||||
$bootable{ } \
|
||||
method{ efi } \
|
||||
format{ } \
|
||||
. \
|
||||
1024 10000 -1 ext4 \
|
||||
$primary{ } \
|
||||
method{ format } format{ } \
|
||||
use_filesystem{ } filesystem{ ext4 } \
|
||||
mountpoint{ / } \
|
||||
.
|
||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
|
||||
d-i apt-setup/non-free-firmware boolean true
|
||||
d-i apt-setup/non-free boolean true
|
||||
d-i apt-setup/contrib boolean true
|
||||
|
||||
tasksel tasksel/first multiselect standard, ssh-server
|
||||
d-i pkgsel/include string sudo curl ca-certificates qemu-guest-agent cloud-init
|
||||
d-i pkgsel/upgrade select full-upgrade
|
||||
|
||||
d-i grub-installer/only_debian boolean true
|
||||
d-i grub-installer/bootdev string default
|
||||
|
||||
d-i finish-install/reboot_in_progress note
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Configuring Base System for Cloud Image ==="
|
||||
|
||||
# Install essential kernel modules and utilities
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
socat \
|
||||
conntrack \
|
||||
ipset \
|
||||
iptables \
|
||||
ufw \
|
||||
fail2ban \
|
||||
unattended-upgrades \
|
||||
curl \
|
||||
ca-certificates \
|
||||
git \
|
||||
isc-dhcp-client \
|
||||
tar \
|
||||
gzip
|
||||
|
||||
# Ensure virtio kernel modules are loaded on boot
|
||||
cat << 'EOF' > /etc/modules-load.d/virtio.conf
|
||||
virtio
|
||||
virtio_net
|
||||
virtio_blk
|
||||
virtio_pci
|
||||
virtiofs
|
||||
EOF
|
||||
|
||||
# Configure GRUB for Serial Console (ttyS0 / COM1) and predictable network names
|
||||
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0"/' /etc/default/grub
|
||||
sed -i 's/^#GRUB_TERMINAL=.*/GRUB_TERMINAL="console serial"/' /etc/default/grub
|
||||
echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /etc/default/grub
|
||||
update-grub
|
||||
|
||||
# Enable serial console getty with root auto-login for emergency console recovery
|
||||
mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
|
||||
cat << 'EOF' > /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 %I $TERM
|
||||
EOF
|
||||
systemctl enable serial-getty@ttyS0.service || true
|
||||
|
||||
# Ensure SSH server auto-generates host keys if missing before starting
|
||||
mkdir -p /etc/systemd/system/ssh.service.d
|
||||
cat << 'EOF' > /etc/systemd/system/ssh.service.d/10-generate-host-keys.conf
|
||||
[Service]
|
||||
ExecStartPre=-/usr/bin/ssh-keygen -A
|
||||
EOF
|
||||
|
||||
# Configure ds-identify to always enable cloud-init unconditionally
|
||||
mkdir -p /etc/cloud
|
||||
cat << 'EOF' > /etc/cloud/ds-identify.cfg
|
||||
policy: enabled
|
||||
EOF
|
||||
|
||||
# Configure cloud-init to support all standard cloud datasources (OpenStack, Ec2, NoCloud, ConfigDrive)
|
||||
cat << 'EOF' > /etc/cloud/cloud.cfg.d/99-cloud-init.cfg
|
||||
datasource_list: [ NoCloud, ConfigDrive, OpenStack, Ec2, None ]
|
||||
disable_vm_template: false
|
||||
EOF
|
||||
|
||||
# Explicitly enable cloud-init systemd services
|
||||
systemctl enable cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service || true
|
||||
|
||||
# Ensure automatic DHCP via systemd-networkd and ifupdown
|
||||
mkdir -p /etc/systemd/network
|
||||
cat << 'EOF' > /etc/systemd/network/10-eth.network
|
||||
[Match]
|
||||
Name=eth* en*
|
||||
|
||||
[Network]
|
||||
DHCP=yes
|
||||
EOF
|
||||
systemctl enable systemd-networkd systemd-resolved || true
|
||||
|
||||
# Ensure DHCP on eth0 for legacy ifupdown
|
||||
mkdir -p /etc/network/interfaces.d
|
||||
cat << 'EOF' > /etc/network/interfaces.d/eth0
|
||||
auto eth0
|
||||
iface eth0 inet dhcp
|
||||
EOF
|
||||
|
||||
echo "=== Base System Configuration Complete ==="
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Installing K3s Binary (First-Boot Isolation) ==="
|
||||
|
||||
export INSTALL_K3S_SKIP_ENABLE="true"
|
||||
export INSTALL_K3S_SKIP_START="true"
|
||||
export INSTALL_K3S_VERSION="v1.31.2+k3s1"
|
||||
|
||||
# Download official K3s installer and install binary/systemd unit without starting
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
|
||||
# Verify k3s binary is located in /usr/local/bin
|
||||
which k3s
|
||||
k3s --version
|
||||
|
||||
# Ensure systemd service is NOT running
|
||||
systemctl stop k3s.service || true
|
||||
systemctl disable k3s.service || true
|
||||
|
||||
# Create configuration directory
|
||||
mkdir -p /etc/rancher/k3s
|
||||
|
||||
echo "=== K3s Pre-installation Complete ==="
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Sanitizing Machine Identity & Temporary Files ==="
|
||||
|
||||
# Clean APT caches and logs
|
||||
apt-get autoremove -y --purge
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Reset machine-id for unique DHCP lease acquisition on clone
|
||||
truncate -s 0 /etc/machine-id
|
||||
rm -f /var/lib/dbus/machine-id
|
||||
ln -sf /etc/machine-id /var/lib/dbus/machine-id
|
||||
|
||||
# Ensure SSH host keys are generated and valid
|
||||
ssh-keygen -A
|
||||
|
||||
# Reset DNS with reliable fallback resolvers
|
||||
rm -f /etc/cloud/cloud-init.disabled /run/systemd/generator*/cloud-init.disabled
|
||||
cat << 'EOF' > /etc/resolv.conf
|
||||
nameserver 1.1.1.1
|
||||
nameserver 8.8.8.8
|
||||
nameserver 77.88.8.8
|
||||
EOF
|
||||
cloud-init clean --logs --seed || true
|
||||
rm -rf /var/lib/cloud/*
|
||||
|
||||
# Clean bash history
|
||||
rm -f /root/.bash_history /home/*/.bash_history
|
||||
|
||||
# Sync filesystem
|
||||
sync
|
||||
|
||||
echo "=== Cleanup Complete ==="
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Usage: ./deploy-certbot.sh [domain1] [domain2] ...
|
||||
EMAIL="personal@ermnvldmr.com"
|
||||
CLOUDFLARE_INI="/etc/letsencrypt/cloudflare.ini"
|
||||
|
||||
# Collect domains from arguments, fallback to default if none provided
|
||||
if [ $# -eq 0 ]; then
|
||||
DOMAINS=("docs.ermnvldmr.com")
|
||||
else
|
||||
DOMAINS=("$@")
|
||||
fi
|
||||
|
||||
# Build domain arguments for certbot
|
||||
DOMAIN_ARGS=()
|
||||
for d in "${DOMAINS[@]}"; do
|
||||
DOMAIN_ARGS+=("-d" "$d")
|
||||
done
|
||||
|
||||
echo "Installing certbot and Cloudflare DNS plugin..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y certbot python3-certbot-cloudflare
|
||||
|
||||
if [ ! -f "$CLOUDFLARE_INI" ]; then
|
||||
echo "Error: Cloudflare credentials file not found at $CLOUDFLARE_INI"
|
||||
echo "Please ensure it exists and has 600 permissions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Requesting certificate for ${DOMAINS[*]} via Cloudflare DNS challenge..."
|
||||
sudo certbot certonly \
|
||||
--dns-cloudflare \
|
||||
--dns-cloudflare-credentials "$CLOUDFLARE_INI" \
|
||||
"${DOMAIN_ARGS[@]}" \
|
||||
--email "$EMAIL" \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--non-interactive
|
||||
|
||||
echo "Certificates for ${DOMAINS[*]} have been processed."
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE=$1
|
||||
TAG=$2
|
||||
|
||||
if [ -z "$SERVICE" ] || [ -z "$TAG" ]; then
|
||||
echo "Usage: $0 <service: www|docs|umami> <tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
APP_DIR="$ROOT_DIR/ci/k8s/apps/$SERVICE"
|
||||
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "Error: Application directory '$APP_DIR' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating Kustomize image tag for '$SERVICE' to '$TAG'..."
|
||||
cd "$APP_DIR"
|
||||
kustomize edit set image "ghcr.io/deytenit/ermnvldmr.com/${SERVICE}:${TAG}"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
git config user.name "adam-at-deytenit"
|
||||
git config user.email "221188529+adam-at-deytenit@users.noreply.github.com"
|
||||
git add "ci/k8s/apps/${SERVICE}/kustomization.yaml"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No manifest changes to commit for $SERVICE."
|
||||
else
|
||||
git commit -m "chore(release): deploy ${SERVICE} ${TAG} [skip ci]"
|
||||
git push origin next
|
||||
echo "Successfully deployed $SERVICE ($TAG) via GitOps."
|
||||
fi
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Setup paths relative to the script location
|
||||
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../deploy" && pwd)"
|
||||
ENV_FILE="$DEPLOY_DIR/.env"
|
||||
TEMPLATE_FILE="$DEPLOY_DIR/.env.template"
|
||||
|
||||
echo "Starting deployment for com-ermnvldmr..."
|
||||
|
||||
# 1. Load existing .env values as defaults (do NOT overwrite environment variables)
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$line" =~ ^# ]] || [[ -z "$line" ]] && continue
|
||||
|
||||
key="${line%%=*}"
|
||||
val="${line#*=}"
|
||||
|
||||
# Only export if the variable is not already set in the environment
|
||||
if [ -z "${!key}" ]; then
|
||||
export "$key"="$val"
|
||||
fi
|
||||
done < "$ENV_FILE"
|
||||
fi
|
||||
|
||||
# 2. Apply final fallback defaults
|
||||
export DOCS_TAG="${DOCS_TAG:-latest}"
|
||||
export WWW_TAG="${WWW_TAG:-latest}"
|
||||
|
||||
# 3. Generate the .env file from template
|
||||
# Explicitly list variables to substitute to avoid messing with other values
|
||||
echo "Generating .env from template..."
|
||||
envsubst '$DOCS_TAG $WWW_TAG' < "$TEMPLATE_FILE" > "$ENV_FILE"
|
||||
|
||||
# 4. Run Docker operations
|
||||
cd "$DEPLOY_DIR"
|
||||
echo "Pulling images..."
|
||||
docker compose pull
|
||||
|
||||
echo "Starting containers..."
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
echo "Pruning unused images..."
|
||||
docker image prune -f
|
||||
|
||||
echo "Deployment complete."
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STATIC_DIR="$SCRIPT_DIR/../../services/www/public/static"
|
||||
|
||||
if [ ! -d "$STATIC_DIR" ]; then
|
||||
echo "Directory $STATIC_DIR does not exist. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Optimizing images in $STATIC_DIR using cwebp..."
|
||||
|
||||
# Find all png, jpg, jpeg files
|
||||
find "$STATIC_DIR" -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) | while read -r file; do
|
||||
# Determine output path (replace extension with .webp)
|
||||
dir=$(dirname "$file")
|
||||
base=$(basename "$file")
|
||||
ext="${base##*.}"
|
||||
name="${base%.*}"
|
||||
output="$dir/$name.webp"
|
||||
|
||||
# Check if webp already exists and is newer
|
||||
if [ -f "$output" ] && [ "$output" -nt "$file" ]; then
|
||||
echo "Skipping $file (already optimized)"
|
||||
else
|
||||
echo "Optimizing: $file -> $output"
|
||||
cwebp -q 80 "$file" -o "$output"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Image optimization complete!"
|
||||
@@ -3,10 +3,9 @@ set -e
|
||||
|
||||
MODE=$1
|
||||
TARGET=$2
|
||||
INCREMENT_MAJOR=${3:-false}
|
||||
|
||||
if [ -z "$MODE" ] || [ -z "$TARGET" ]; then
|
||||
echo "Usage: $0 <mode: checkout|rc|release> <target: service_path|branch_name> [increment_major: true|false]"
|
||||
echo "Usage: $0 <mode: checkout|rc|release> <target: service_path|branch_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -14,56 +13,9 @@ case $MODE in
|
||||
checkout)
|
||||
SERVICE_PATH=$TARGET
|
||||
SERVICE_NAME=$(basename "$SERVICE_PATH")
|
||||
|
||||
# Ensure we have the latest tags and branches
|
||||
git fetch --all --tags --quiet 2>/dev/null || true
|
||||
|
||||
BRANCH_NAME=$(node -e '
|
||||
const { execSync } = require("child_process");
|
||||
const serviceName = process.argv[1];
|
||||
const servicePath = process.argv[2];
|
||||
const incrementMajor = process.argv[3] === "true" || process.argv[3] === "major" || process.argv[3] === "1";
|
||||
|
||||
let rawBranchesAndTags = "";
|
||||
try {
|
||||
const remoteBranches = execSync(`git branch -r --list "origin/releases/${serviceName}/v*"`, { encoding: "utf8" });
|
||||
const localBranches = execSync(`git branch --list "releases/${serviceName}/v*"`, { encoding: "utf8" });
|
||||
const tags = execSync(`git tag -l "releases/${serviceName}/v*"`, { encoding: "utf8" });
|
||||
rawBranchesAndTags = `${remoteBranches}\n${localBranches}\n${tags}`;
|
||||
} catch (e) {}
|
||||
|
||||
const versionMatches = Array.from(rawBranchesAndTags.matchAll(/releases\/[^\/]+\/v([0-9]+)\.([0-9]+)/g));
|
||||
|
||||
if (versionMatches.length === 0) {
|
||||
let pkgVersion = "1.0";
|
||||
try {
|
||||
pkgVersion = require(`./${servicePath}/package.json`).version.split(".").slice(0, 2).join(".");
|
||||
} catch (e) {}
|
||||
console.log(`releases/${serviceName}/v${pkgVersion}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let maxMajor = 1;
|
||||
let maxMinor = 0;
|
||||
for (const match of versionMatches) {
|
||||
const maj = parseInt(match[1], 10);
|
||||
const min = parseInt(match[2], 10);
|
||||
if (!isNaN(maj) && !isNaN(min)) {
|
||||
if (maj > maxMajor || (maj === maxMajor && min > maxMinor)) {
|
||||
maxMajor = maj;
|
||||
maxMinor = min;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (incrementMajor) {
|
||||
console.log(`releases/${serviceName}/v${maxMajor + 1}.0`);
|
||||
} else {
|
||||
console.log(`releases/${serviceName}/v${maxMajor}.${maxMinor + 1}`);
|
||||
}
|
||||
' "$SERVICE_NAME" "$SERVICE_PATH" "$INCREMENT_MAJOR")
|
||||
|
||||
echo "$BRANCH_NAME"
|
||||
VERSION=$(node -p "require('./$SERVICE_PATH/package.json').version")
|
||||
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
|
||||
echo "releases/$SERVICE_NAME/v$MAJOR_MINOR"
|
||||
;;
|
||||
rc|release)
|
||||
BRANCH_NAME=$TARGET
|
||||
@@ -74,24 +26,24 @@ case $MODE in
|
||||
fi
|
||||
SERVICE_NAME="${BASH_REMATCH[1]}"
|
||||
MAJOR_MINOR="${BASH_REMATCH[2]}"
|
||||
|
||||
|
||||
TAG_PREFIX="releases/$SERVICE_NAME/v$MAJOR_MINOR"
|
||||
|
||||
|
||||
# Ensure we have the latest tags
|
||||
git fetch --tags --quiet
|
||||
|
||||
|
||||
# Find the latest patch version for this Major.Minor
|
||||
LATEST_TAG=$(git tag -l "$TAG_PREFIX.*" | grep -v "\-rc" | sort -V | tail -n1 || echo "")
|
||||
|
||||
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
PATCH=0
|
||||
else
|
||||
LATEST_VERSION=${LATEST_TAG#"$TAG_PREFIX."}
|
||||
PATCH=$((LATEST_VERSION + 1))
|
||||
fi
|
||||
|
||||
|
||||
VERSION_TAG="$MAJOR_MINOR.$PATCH"
|
||||
|
||||
|
||||
if [ "$MODE" == "release" ]; then
|
||||
echo "$VERSION_TAG"
|
||||
else
|
||||
|
||||
+5
-10
@@ -23,10 +23,6 @@
|
||||
"ui:build": "pnpm --filter @ermnvldmr/ui build",
|
||||
"ui:test": "pnpm --filter @ermnvldmr/ui test",
|
||||
"ui:storybook": "pnpm --filter @ermnvldmr/ui storybook",
|
||||
"kits:dev": "pnpm --filter @ermnvldmr/kits dev",
|
||||
"kits:build": "pnpm --filter @ermnvldmr/kits build",
|
||||
"kits:test": "pnpm --filter @ermnvldmr/kits test",
|
||||
"kits:storybook": "pnpm --filter @ermnvldmr/kits storybook",
|
||||
"www:dev": "pnpm --filter @ermnvldmr/www dev",
|
||||
"www:build": "pnpm --filter @ermnvldmr/www build",
|
||||
"www:storybook": "pnpm --filter @ermnvldmr/www storybook",
|
||||
@@ -37,19 +33,18 @@
|
||||
"static:lint": "pnpm --filter @ermnvldmr/static lint",
|
||||
"static:typecheck": "pnpm --filter @ermnvldmr/static typecheck",
|
||||
"dev:ui": "pnpm ui:dev",
|
||||
"dev:kits": "pnpm kits:dev",
|
||||
"dev:www": "pnpm ui:build && pnpm kits:build && pnpm www:dev",
|
||||
"dev:static": "pnpm ui:build && pnpm kits:build && pnpm static:dev",
|
||||
"dev:www": "pnpm ui:build && pnpm www:dev",
|
||||
"dev:static": "pnpm ui:build && pnpm static:dev",
|
||||
"dev:docs": "pnpm docs:dev",
|
||||
"build": "pnpm ui:build && pnpm --filter @ermnvldmr/kits build && pnpm -r --filter='./services/*' build",
|
||||
"build:storybooks": "pnpm ui:build && pnpm --filter @ermnvldmr/kits build && pnpm --filter @ermnvldmr/ui build-storybook && pnpm --filter @ermnvldmr/kits build-storybook && pnpm --filter @ermnvldmr/www build-storybook",
|
||||
"build": "pnpm ui:build && pnpm -r --filter='./services/*' build",
|
||||
"build:storybooks": "pnpm ui:build && pnpm --filter @ermnvldmr/ui build-storybook && pnpm --filter @ermnvldmr/www build-storybook",
|
||||
"lint": "pnpm -r lint",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test": "pnpm -r test",
|
||||
"test:coverage": "pnpm -r test:coverage",
|
||||
"clean": "pnpm -r clean && rm -rf node_modules",
|
||||
"build:deps": "pnpm -r --filter='./packages/*-config' build && pnpm --filter='./packages/stl' build && pnpm --filter='./packages/cli' build && pnpm --filter='./packages/ssg' build && pnpm --filter='./packages/i18n' build && pnpm --filter='./packages/ui' build && pnpm --filter='./packages/kits' build"
|
||||
"build:deps": "pnpm -r --filter='./packages/*-config' build && pnpm --filter='./packages/i18n' build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"globals": "^16.0.0",
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
# @ermnvldmr/cli
|
||||
|
||||
An extensible, configuration-driven CLI toolkit designed specifically for the ermnvldmr monorepo.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@ermnvldmr/cli` package provides a unified way to declare, manage, and execute scripts across different packages and services in the monorepo. Instead of cluttering `package.json` with complex bash scripts or maintaining ad-hoc Node.js script files, developers can define their package's commands in a strongly-typed `.config/cli.ts` file.
|
||||
|
||||
Under the hood, it uses [Commander.js](https://github.com/tj/commander.js) for argument parsing and [JITI](https://github.com/unjs/jiti) for seamless, dynamic TypeScript execution.
|
||||
|
||||
## Features
|
||||
|
||||
- **Centralized Configuration:** Define all commands in a single `.config/cli.ts` file per package.
|
||||
- **Type-Safe Builders:** Use `bashCommand` and `nodeCommand` builders to get strict TypeScript inference.
|
||||
- **Bash & Node Support:** Easily execute shell scripts (via `execa`) or arbitrary asynchronous Node.js functions.
|
||||
- **Commander Integration:** Automatically registers commands, descriptions, and arguments into a robust CLI interface.
|
||||
|
||||
## Installation
|
||||
|
||||
This package is intended to be used as a development dependency within the monorepo's workspace packages.
|
||||
|
||||
```bash
|
||||
pnpm add -D @ermnvldmr/cli --workspace
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Create a Configuration File
|
||||
|
||||
Create a `.config/cli.ts` file in the root of your package/service (e.g., `packages/ui/.config/cli.ts`).
|
||||
|
||||
```typescript
|
||||
import { defineCli, bashCommand, nodeCommand } from '@ermnvldmr/cli';
|
||||
|
||||
export default defineCli({
|
||||
commands: [
|
||||
bashCommand({
|
||||
name: 'hello',
|
||||
description: 'Prints a hello message via bash',
|
||||
script: 'echo "Hello from Bash!"'
|
||||
}),
|
||||
|
||||
nodeCommand({
|
||||
name: 'build:custom',
|
||||
description: 'Runs a custom node build step',
|
||||
options: [
|
||||
{
|
||||
flags: '--minify',
|
||||
description: 'Enable minification',
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
action: async (options) => {
|
||||
console.log('Building with node...');
|
||||
if (options.minify) {
|
||||
console.log('Minification enabled!');
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Run the CLI
|
||||
|
||||
You can invoke the CLI directly using `pnpm exec` from the package directory:
|
||||
|
||||
```bash
|
||||
pnpm exec ermnvldmr-cli hello
|
||||
pnpm exec ermnvldmr-cli build:custom --minify
|
||||
```
|
||||
|
||||
Alternatively, you can wire it into your `package.json` scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"cli": "ermnvldmr-cli",
|
||||
"hello": "ermnvldmr-cli hello"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `defineCli(config: CliConfig)`
|
||||
|
||||
An identity helper function that provides strict TypeScript type-inference for your configuration object. It returns the configuration unmodified.
|
||||
|
||||
### `bashCommand(config: Omit<BashCommandConfig, 'type'>)`
|
||||
|
||||
A builder function for defining shell commands.
|
||||
- **`script`**: The exact bash string to execute. It will be executed using `execa` with `shell: true` and `stdio: 'inherit'`.
|
||||
|
||||
### `nodeCommand(config: Omit<NodeCommandConfig, 'type'>)`
|
||||
|
||||
A builder function for defining arbitrary JavaScript/TypeScript actions.
|
||||
- **`action`**: An asynchronous function `(options: Record<string, unknown>) => Promise<void> | void` that executes when the command is invoked.
|
||||
|
||||
## Development
|
||||
|
||||
This package adheres to the global monorepo standards outlined in `GEMINI.md`:
|
||||
- **Strict TypeScript**: No `any` types are permitted.
|
||||
- **TSDoc**: All exported members must include comprehensive TSDoc comments.
|
||||
- **TDD**: Modifications and new builders should be accompanied by Vitest unit tests.
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "@ermnvldmr/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Extensible CLI tool for ermnvldmr projects",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"ermnvldmr-cli": "./dist/bin.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1075.0",
|
||||
"@dotenvx/dotenvx": "^1.75.1",
|
||||
"commander": "^13.1.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"jiti": "^2.4.2",
|
||||
"mime-types": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/rsbuild-config": "workspace:*",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^22.13.0",
|
||||
"sharp": "^0.35.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runCli } from './lib/runner.js';
|
||||
|
||||
runCli().catch((err) => {
|
||||
console.error('An unexpected error occurred:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { defineCli, bashCommand, nodeCommand } from './index.js';
|
||||
|
||||
describe('public exports', () => {
|
||||
it('defineCli returns the passed config unmodified', () => {
|
||||
const config = defineCli({
|
||||
commands: [],
|
||||
});
|
||||
expect(config.commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('exports builders', () => {
|
||||
expect(typeof bashCommand).toBe('function');
|
||||
expect(typeof nodeCommand).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { CliConfig } from './lib/types.js';
|
||||
|
||||
/**
|
||||
* Helper function to define the CLI configuration with strict type-safety.
|
||||
*
|
||||
* @param config - The CLI configuration containing the array of commands.
|
||||
* @returns The unmodified CLI configuration.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = defineCli({
|
||||
* commands: [
|
||||
* bashCommand({ name: 'hello', description: 'say hello', script: 'echo hello' })
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function defineCli(config: CliConfig): CliConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export { bashCommand } from './lib/builders/bash.js';
|
||||
export { nodeCommand } from './lib/builders/node.js';
|
||||
export { s3SyncCommand } from './lib/builders/s3Sync.js';
|
||||
export { env } from './lib/env.js';
|
||||
export * from './lib/types.js';
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { bashCommand } from './bash.js';
|
||||
|
||||
describe('bashCommand builder', () => {
|
||||
it('should return a properly formatted bash command config', () => {
|
||||
const config = bashCommand({
|
||||
name: 'hello',
|
||||
description: 'says hello',
|
||||
script: 'echo "hello"',
|
||||
});
|
||||
|
||||
expect(config.name).toBe('hello');
|
||||
expect(config.description).toBe('says hello');
|
||||
expect(config.script).toBe('echo "hello"');
|
||||
expect(config.type).toBe('bash');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { BashCommandConfig } from '../types.js';
|
||||
|
||||
/**
|
||||
* Creates a Bash command configuration.
|
||||
*
|
||||
* @param config - The command configuration excluding the 'type' field.
|
||||
* @returns The fully formatted Bash command configuration.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = bashCommand({
|
||||
* name: 'hello',
|
||||
* description: 'says hello',
|
||||
* script: 'echo "hello"'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function bashCommand(
|
||||
config: Omit<BashCommandConfig, 'type'>
|
||||
): BashCommandConfig {
|
||||
return {
|
||||
...config,
|
||||
type: 'bash',
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { nodeCommand } from './node.js';
|
||||
|
||||
describe('nodeCommand builder', () => {
|
||||
it('should return a properly formatted node command config', () => {
|
||||
const action = async () => {};
|
||||
const config = nodeCommand({
|
||||
name: 'build',
|
||||
description: 'builds stuff',
|
||||
action,
|
||||
});
|
||||
|
||||
expect(config.name).toBe('build');
|
||||
expect(config.description).toBe('builds stuff');
|
||||
expect(config.action).toBe(action);
|
||||
expect(config.type).toBe('node');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { NodeCommandConfig } from '../types.js';
|
||||
|
||||
/**
|
||||
* Creates a Node command configuration.
|
||||
*
|
||||
* @param config - The command configuration excluding the 'type' field.
|
||||
* @returns The fully formatted Node command configuration.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = nodeCommand({
|
||||
* name: 'run',
|
||||
* description: 'runs action',
|
||||
* action: () => console.log('hello')
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function nodeCommand(
|
||||
config: Omit<NodeCommandConfig, 'type'>
|
||||
): NodeCommandConfig {
|
||||
return {
|
||||
...config,
|
||||
type: 'node',
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { s3SyncCommand } from './s3Sync.js';
|
||||
|
||||
describe('s3SyncCommand', () => {
|
||||
it('should create an s3-sync command config', () => {
|
||||
const config = s3SyncCommand({
|
||||
name: 'sync',
|
||||
description: 'Sync files',
|
||||
targetPrefix: 'www'
|
||||
});
|
||||
|
||||
expect(config.type).toBe('s3-sync');
|
||||
expect(config.name).toBe('sync');
|
||||
expect(config.targetPrefix).toBe('www');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { S3SyncCommandConfig } from '../types.js';
|
||||
|
||||
/**
|
||||
* Creates an S3 Sync command configuration.
|
||||
*
|
||||
* @param config - The command configuration excluding the 'type' field.
|
||||
* @returns The fully formatted S3 Sync command configuration.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = s3SyncCommand({
|
||||
* name: 'sync',
|
||||
* description: 'Sync files',
|
||||
* targetPrefix: 'www'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function s3SyncCommand(
|
||||
config: Omit<S3SyncCommandConfig, 'type'>
|
||||
): S3SyncCommandConfig {
|
||||
return {
|
||||
...config,
|
||||
type: 's3-sync',
|
||||
};
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock dotenvx before importing env
|
||||
vi.mock('@dotenvx/dotenvx', () => ({
|
||||
config: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock node:fs so findRoot returns a predictable root
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn((p: string) => String(p).endsWith('pnpm-workspace.yaml')),
|
||||
}));
|
||||
|
||||
describe('env', () => {
|
||||
beforeEach(() => {
|
||||
// Reset module registry so the `loaded` singleton resets between tests
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('calls dotenvx config() on first access with the root .env.dev path', async () => {
|
||||
const { config } = await import('@dotenvx/dotenvx');
|
||||
const { env } = await import('./env.js');
|
||||
|
||||
process.env.TEST_VAR = 'hello';
|
||||
env('TEST_VAR');
|
||||
|
||||
expect(config).toHaveBeenCalledTimes(1);
|
||||
const firstCall = vi.mocked(config).mock.calls[0];
|
||||
expect(firstCall[0]).toMatchObject({ path: expect.stringContaining('.env.dev') });
|
||||
delete process.env.TEST_VAR;
|
||||
});
|
||||
|
||||
it('returns the environment variable value', async () => {
|
||||
const { env } = await import('./env.js');
|
||||
|
||||
process.env.MY_KEY = 'my-value';
|
||||
expect(env('MY_KEY')).toBe('my-value');
|
||||
delete process.env.MY_KEY;
|
||||
});
|
||||
|
||||
it('returns the fallback when the variable is not set', async () => {
|
||||
const { env } = await import('./env.js');
|
||||
|
||||
delete process.env.MISSING_KEY;
|
||||
expect(env('MISSING_KEY', 'fallback-value')).toBe('fallback-value');
|
||||
});
|
||||
|
||||
it('returns undefined when the variable is not set and no fallback is provided', async () => {
|
||||
const { env } = await import('./env.js');
|
||||
|
||||
delete process.env.MISSING_KEY;
|
||||
expect(env('MISSING_KEY')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('calls dotenvx config() only once across multiple env() calls within the same import', async () => {
|
||||
const { config } = await import('@dotenvx/dotenvx');
|
||||
const mockConfig = vi.mocked(config);
|
||||
mockConfig.mockClear();
|
||||
|
||||
const { env } = await import('./env.js');
|
||||
|
||||
env('FOO');
|
||||
env('BAR');
|
||||
env('BAZ');
|
||||
|
||||
// config() should have been called exactly once for this fresh module instance
|
||||
expect(mockConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { config } from '@dotenvx/dotenvx';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
/**
|
||||
* Walks up the directory tree from `start` until it finds a directory
|
||||
* containing `pnpm-workspace.yaml` (the monorepo root), or reaches the
|
||||
* filesystem root.
|
||||
*
|
||||
* @param start - Directory to begin the search from.
|
||||
* @returns The path of the monorepo root directory, or `start` as a fallback.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const root = findRoot(process.cwd());
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function findRoot(start: string): string {
|
||||
let dir = start;
|
||||
for (;;) {
|
||||
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
||||
return dir;
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) {
|
||||
// Reached the filesystem root without finding the marker.
|
||||
return start;
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures dotenvx has loaded `.env.dev` from the monorepo root into `process.env`.
|
||||
* Walks up from the current working directory to find the root, then loads the
|
||||
* `.env.dev` file located there. Safe to call multiple times — runs only once per
|
||||
* process.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ensureLoaded();
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function ensureLoaded(): void {
|
||||
if (!loaded) {
|
||||
config({ path: join(findRoot(process.cwd()), '.env.dev') });
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an environment variable by key, loading `.env.dev` files via dotenvx on
|
||||
* the first call. Optionally returns a fallback value when the variable is
|
||||
* not set.
|
||||
*
|
||||
* Designed for use inside `cli.ts` config files so that env values are
|
||||
* resolved **at command execution time** (after dotenvx has populated
|
||||
* `process.env`), not at config-parse time.
|
||||
*
|
||||
* @param key - The name of the environment variable to read.
|
||||
* @param fallback - Optional fallback value returned when the variable is absent.
|
||||
* @returns The environment variable value, the fallback, or `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { env } from '@ermnvldmr/cli';
|
||||
*
|
||||
* s3SyncCommand({
|
||||
* bucket: env('MY_BUCKET'),
|
||||
* region: env('AWS_REGION', 'us-east-1'),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function env(key: string, fallback?: string): string | undefined {
|
||||
ensureLoaded();
|
||||
return process.env[key] ?? fallback;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { registerCommands } from './runner.js';
|
||||
import { runS3Sync } from './runners/s3SyncRunner.js';
|
||||
|
||||
vi.mock('./runners/s3SyncRunner.js', () => ({
|
||||
runS3Sync: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('registerCommands', () => {
|
||||
it('registers commands into a Commander instance', async () => {
|
||||
const program = new Command();
|
||||
const actionSpy = vi.fn();
|
||||
|
||||
registerCommands(program, {
|
||||
commands: [
|
||||
{
|
||||
type: 'node',
|
||||
name: 'test-cmd',
|
||||
description: 'A test command',
|
||||
action: actionSpy,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// We simulate parsing args to run the action. The registered action is
|
||||
// async, so parseAsync + await ensures it runs before we assert.
|
||||
await program.parseAsync(['node', 'script', 'test-cmd']);
|
||||
expect(actionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers s3-sync commands into a Commander instance', async () => {
|
||||
const program = new Command();
|
||||
const cmd = {
|
||||
type: 's3-sync' as const,
|
||||
name: 'sync-cmd',
|
||||
description: 'An S3 sync command',
|
||||
targetPrefix: 'my-site',
|
||||
};
|
||||
|
||||
registerCommands(program, {
|
||||
commands: [cmd]
|
||||
});
|
||||
|
||||
await program.parseAsync(['node', 'script', 'sync-cmd']);
|
||||
expect(runS3Sync).toHaveBeenCalledWith(cmd, expect.any(Object));
|
||||
});
|
||||
});
|
||||
@@ -1,119 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { execaCommand } from 'execa';
|
||||
import { createJiti } from 'jiti';
|
||||
|
||||
import type { CliConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* Registers commands from the configuration into a Commander program.
|
||||
*
|
||||
* @param program - The Commander program instance.
|
||||
* @param config - The CLI configuration containing the commands to register.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Command } from 'commander';
|
||||
* const program = new Command();
|
||||
* registerCommands(program, { commands: [] });
|
||||
* ```
|
||||
*/
|
||||
export function registerCommands(program: Command, config: CliConfig): void {
|
||||
for (const cmd of config.commands) {
|
||||
const command = program.command(cmd.name).description(cmd.description);
|
||||
|
||||
if (cmd.options) {
|
||||
for (const opt of cmd.options) {
|
||||
command.option(opt.flags, opt.description, opt.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
switch (cmd.type) {
|
||||
case 'bash': {
|
||||
command.action(async () => {
|
||||
try {
|
||||
await execaCommand(cmd.script, { stdio: 'inherit', shell: true });
|
||||
} catch (err: unknown) {
|
||||
const isErrorWithExitCode = (e: unknown): e is { exitCode: number } => {
|
||||
return typeof e === 'object' && e !== null && 'exitCode' in e && typeof Reflect.get(e, 'exitCode') === 'number';
|
||||
};
|
||||
process.exit(isErrorWithExitCode(err) ? err.exitCode : 1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'node': {
|
||||
command.action(async (options: Record<string, unknown>) => {
|
||||
try {
|
||||
await cmd.action(options);
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 's3-sync': {
|
||||
command.action(async (options: Record<string, unknown>) => {
|
||||
try {
|
||||
const { runS3Sync } = await import('./runners/s3SyncRunner.js');
|
||||
await runS3Sync(cmd, options);
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = cmd;
|
||||
void _exhaustiveCheck;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point function that loads the CLI configuration and executes the runner.
|
||||
*
|
||||
* @param cwd - Current working directory (defaults to process.cwd()).
|
||||
* @param argv - Process arguments (defaults to process.argv).
|
||||
* @example
|
||||
* ```typescript
|
||||
* await runCli();
|
||||
* ```
|
||||
*/
|
||||
export async function runCli(cwd: string = process.cwd(), argv: string[] = process.argv): Promise<void> {
|
||||
const configPath = path.join(cwd, '.config/cli.ts');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`Error: No .config/cli.ts found in ${cwd}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jiti = createJiti(import.meta.url);
|
||||
const rawModule = await jiti.import(configPath);
|
||||
|
||||
const isCliConfigModule = (mod: unknown): mod is { default: CliConfig } => {
|
||||
return typeof mod === 'object' && mod !== null && 'default' in mod;
|
||||
};
|
||||
|
||||
if (!isCliConfigModule(rawModule)) {
|
||||
console.error(`Error: Invalid CLI configuration export in ${configPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = rawModule.default;
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.name('ermnvldmr-cli')
|
||||
.description('Extensible CLI runner')
|
||||
.version('1.0.0');
|
||||
|
||||
registerCommands(program, config);
|
||||
|
||||
program.parse(argv);
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
import { runS3Sync } from './s3SyncRunner.js';
|
||||
|
||||
import type { S3SyncCommandConfig } from '../types.js';
|
||||
|
||||
interface MockPutObjectCommandInput {
|
||||
Bucket: string;
|
||||
Key: string;
|
||||
Body: Buffer;
|
||||
ContentType: string;
|
||||
}
|
||||
|
||||
class MockPutObjectCommand {
|
||||
constructor(public input: MockPutObjectCommandInput) {}
|
||||
}
|
||||
|
||||
// Mock dotenvx
|
||||
vi.mock('@dotenvx/dotenvx', () => ({
|
||||
config: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock S3Client
|
||||
const mockSend = vi.fn<(command: MockPutObjectCommand) => Promise<unknown>>();
|
||||
vi.mock('@aws-sdk/client-s3', () => {
|
||||
return {
|
||||
S3Client: class {
|
||||
send = mockSend;
|
||||
},
|
||||
PutObjectCommand: class {
|
||||
constructor(public input: MockPutObjectCommandInput) {}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe('runS3Sync', () => {
|
||||
const tempDir = path.resolve(__dirname, 'temp_test_s3_sync');
|
||||
let cwdSpy: { mockRestore: () => void };
|
||||
|
||||
beforeEach(() => {
|
||||
mockSend.mockReset();
|
||||
|
||||
// Set required environment variables
|
||||
process.env.S3_BUCKET = 'my-bucket';
|
||||
process.env.AWS_REGION = 'us-east-1';
|
||||
process.env.S3_ENDPOINT_URL = 'https://s3.example.com';
|
||||
process.env.AWS_ACCESS_KEY_ID = 'access-key';
|
||||
process.env.AWS_SECRET_ACCESS_KEY = 'secret-key';
|
||||
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
// Mock process.cwd to return tempDir so glob search happens in tempDir/src
|
||||
cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cwdSpy.mockRestore();
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error if required env variables are missing', async () => {
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'test',
|
||||
type: 's3-sync'
|
||||
};
|
||||
|
||||
// Clear env vars
|
||||
delete process.env.S3_BUCKET;
|
||||
|
||||
await expect(runS3Sync(cmd, {})).rejects.toThrow('Missing required S3 environment variables');
|
||||
});
|
||||
|
||||
it('should successfully sync files using default sourceDir and replace references in source files', async () => {
|
||||
// Default sourceDir is public/static
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
|
||||
// Create dummy source folder and files (where references will be replaced)
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
const htmlSourcePath = path.join(srcDir, 'index.tsx');
|
||||
const cssSourcePath = path.join(srcDir, 'style.css');
|
||||
|
||||
fs.writeFileSync(htmlSourcePath, 'const img = "/static/logo.png";');
|
||||
fs.writeFileSync(cssSourcePath, 'body { background: url(/static/logo.png); }');
|
||||
|
||||
// Create dummy files inside the static directory to upload
|
||||
const imagePath = path.join(publicStaticDir, 'logo.png');
|
||||
fs.writeFileSync(imagePath, 'binary-content-mock');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'my-site',
|
||||
type: 's3-sync',
|
||||
};
|
||||
|
||||
// Run sync (which should default to public/static)
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
// Check if the HTML and CSS source file references were replaced
|
||||
const htmlContent = fs.readFileSync(htmlSourcePath, 'utf-8');
|
||||
const cssContent = fs.readFileSync(cssSourcePath, 'utf-8');
|
||||
|
||||
expect(htmlContent).toContain('https://s3.example.com/my-bucket/my-site/logo.png');
|
||||
expect(cssContent).toContain('https://s3.example.com/my-bucket/my-site/logo.png');
|
||||
|
||||
// Check if S3Client sent PutObjectCommands for the file
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
|
||||
const calls = mockSend.mock.calls;
|
||||
const commandInstance = calls[0][0];
|
||||
expect(commandInstance.input.Key).toBe('my-site/logo.png');
|
||||
});
|
||||
|
||||
it('should replace longer overlapping filenames before shorter ones', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
const sourcePath = path.join(srcDir, 'index.tsx');
|
||||
fs.writeFileSync(sourcePath, 'const img1 = "/static/logo.png.webp"; const img2 = "/static/logo.png";');
|
||||
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'logo.png.webp'), 'webp-mock');
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'logo.png'), 'png-mock');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'my-site',
|
||||
type: 's3-sync',
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
const content = fs.readFileSync(sourcePath, 'utf-8');
|
||||
expect(content).toContain('https://s3.example.com/my-bucket/my-site/logo.png.webp');
|
||||
expect(content).toContain('https://s3.example.com/my-bucket/my-site/logo.png');
|
||||
expect(content).not.toContain('/static/logo.png.webp');
|
||||
expect(content).not.toContain('/static/logo.png');
|
||||
});
|
||||
|
||||
it('should respect beforeUpload hook if provided', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
|
||||
const htmlPath = path.join(publicStaticDir, 'index.html');
|
||||
fs.writeFileSync(htmlPath, '<html><body>Hello</body></html>');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'my-site',
|
||||
type: 's3-sync',
|
||||
hooks: {
|
||||
beforeUpload: (files) => {
|
||||
const processed = files.map(file => {
|
||||
const ext = path.extname(file);
|
||||
const base = path.basename(file, ext);
|
||||
return {
|
||||
originalName: path.basename(file),
|
||||
finalName: `custom-${base}${ext}`,
|
||||
absolutePath: file,
|
||||
};
|
||||
});
|
||||
return Promise.resolve(processed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
const commandInstance = mockSend.mock.calls[0][0];
|
||||
expect(commandInstance.input.Key).toBe('my-site/custom-index.html');
|
||||
});
|
||||
|
||||
it('should resolve S3 configuration parameters passed directly via configuration object', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
|
||||
// Write a dummy file to upload so the upload loop runs
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'test.txt'), 'content');
|
||||
|
||||
// Clear standard env vars to ensure fallback is not used
|
||||
delete process.env.S3_BUCKET;
|
||||
delete process.env.AWS_REGION;
|
||||
delete process.env.AWS_ACCESS_KEY_ID;
|
||||
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'test',
|
||||
bucket: 'direct-bucket',
|
||||
region: 'us-west-2',
|
||||
accessKeyId: 'direct-access-key',
|
||||
secretAccessKey: 'direct-secret-key',
|
||||
type: 's3-sync'
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
const commandInstance = mockSend.mock.calls[0][0];
|
||||
expect(commandInstance.input.Bucket).toBe('direct-bucket');
|
||||
});
|
||||
|
||||
it('should run beforeSync hook as shell command if provided as string', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'test.txt'), 'content');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'test',
|
||||
type: 's3-sync',
|
||||
hooks: {
|
||||
beforeSync: 'echo "hello from beforeSync script"',
|
||||
},
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run beforeSync hook as async function if provided as function', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'test.txt'), 'content');
|
||||
|
||||
const beforeSyncFn = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'test',
|
||||
type: 's3-sync',
|
||||
hooks: {
|
||||
beforeSync: beforeSyncFn,
|
||||
},
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
expect(beforeSyncFn).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should automatically map png/jpg/jpeg to webp if counterpart exists and deduplicate uploads', async () => {
|
||||
const publicStaticDir = path.join(tempDir, 'public/static');
|
||||
fs.mkdirSync(publicStaticDir, { recursive: true });
|
||||
|
||||
// Create png and webp counterpart
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'logo.png'), 'png-content');
|
||||
fs.writeFileSync(path.join(publicStaticDir, 'logo.webp'), 'webp-content');
|
||||
|
||||
// Create a source file referencing the png
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
const sourcePath = path.join(srcDir, 'index.tsx');
|
||||
fs.writeFileSync(sourcePath, 'const img = "/static/logo.png";');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
targetPrefix: 'test',
|
||||
type: 's3-sync',
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
// Reference replacement check: /static/logo.png should be replaced by S3 URL of logo.webp
|
||||
const content = fs.readFileSync(sourcePath, 'utf-8');
|
||||
expect(content).toContain('https://s3.example.com/my-bucket/test/logo.webp');
|
||||
expect(content).not.toContain('logo.png');
|
||||
|
||||
// Deduplication check: only logo.webp should have been uploaded once (mockSend called once for logo.webp)
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
const commandInstance = mockSend.mock.calls[0][0];
|
||||
expect(commandInstance.input.Key).toBe('test/logo.webp');
|
||||
});
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { execaCommand } from 'execa';
|
||||
import fg from 'fast-glob';
|
||||
import mime from 'mime-types';
|
||||
|
||||
import type { S3SyncCommandConfig, ProcessedFile } from '../types.js';
|
||||
|
||||
/**
|
||||
* Normalizes prefix and name to construct a clean S3 key.
|
||||
*
|
||||
* @param prefix - The target directory prefix.
|
||||
* @param name - The relative path/name of the file.
|
||||
* @returns The clean, normalized S3 key.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* normalizeKey('static', 'js/main.js'); // 'static/js/main.js'
|
||||
* ```
|
||||
*/
|
||||
function normalizeKey(prefix: string, name: string): string {
|
||||
const parts = [prefix, name]
|
||||
.filter(Boolean)
|
||||
.map(p => p.replace(/^\/+|\/+$/g, '').replace(/\\/g, '/'));
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the S3 Sync command.
|
||||
*
|
||||
* Loads environment variables, instantiates S3Client, processes files,
|
||||
* replaces static asset references in source files (`/static/filename` -> S3 URL),
|
||||
* and uploads the assets to S3.
|
||||
*
|
||||
* @param cmd - The configuration for the S3 sync command.
|
||||
* @param options - Additional options passed from Commander.
|
||||
* @returns A promise that resolves when the synchronization is complete.
|
||||
* @throws An error if required environment variables are missing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await runS3Sync(cmd, {});
|
||||
* ```
|
||||
*/
|
||||
export async function runS3Sync(
|
||||
cmd: S3SyncCommandConfig,
|
||||
options: Record<string, unknown> = {}
|
||||
): Promise<void> {
|
||||
void options;
|
||||
|
||||
const bucket = cmd.bucket ?? process.env.S3_BUCKET;
|
||||
const region = cmd.region ?? process.env.AWS_REGION ?? process.env.S3_REGION;
|
||||
const endpoint = cmd.endpoint ?? process.env.S3_ENDPOINT_URL ?? process.env.S3_ENDPOINT;
|
||||
const accessKey = cmd.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? process.env.S3_ACCESS_KEY_ID;
|
||||
const secretKey = cmd.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? process.env.S3_SECRET_ACCESS_KEY;
|
||||
|
||||
if (!bucket || !region || !accessKey || !secretKey) {
|
||||
throw new Error('Missing required S3 environment variables');
|
||||
}
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region,
|
||||
...(endpoint ? { endpoint } : {}),
|
||||
credentials: {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
if (cmd.hooks?.beforeSync) {
|
||||
const hook = cmd.hooks.beforeSync;
|
||||
if (typeof hook === 'string') {
|
||||
console.log(`Running beforeSync script: ${hook}`);
|
||||
await execaCommand(hook, { stdio: 'inherit', shell: true });
|
||||
} else {
|
||||
console.log('Running beforeSync hook...');
|
||||
await hook();
|
||||
}
|
||||
}
|
||||
|
||||
const sourceDir = cmd.sourceDir ?? './public/static';
|
||||
const resolvedSourceDir = path.resolve(process.cwd(), sourceDir);
|
||||
|
||||
if (!fs.existsSync(resolvedSourceDir)) {
|
||||
throw new Error(`Source directory does not exist: ${resolvedSourceDir}`);
|
||||
}
|
||||
|
||||
// Find all files in the source directory, including dotfiles
|
||||
const rawFiles = await fg('**/*', {
|
||||
cwd: resolvedSourceDir,
|
||||
onlyFiles: true,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
});
|
||||
|
||||
// Process files through the hook if it exists, otherwise use the default mapping with auto-optimization detection
|
||||
let processedFiles: ProcessedFile[];
|
||||
if (cmd.hooks?.beforeUpload) {
|
||||
const rawProcessed = await cmd.hooks.beforeUpload(rawFiles);
|
||||
processedFiles = rawProcessed.map((file) => ({
|
||||
...file,
|
||||
originalName: file.originalName.replace(/\\/g, '/'),
|
||||
finalName: file.finalName.replace(/\\/g, '/'),
|
||||
}));
|
||||
} else {
|
||||
// Index raw files by normalized relative path
|
||||
const rawRels = new Set(
|
||||
rawFiles.map((file) => path.relative(resolvedSourceDir, file).replace(/\\/g, '/'))
|
||||
);
|
||||
|
||||
processedFiles = rawFiles.map((file) => {
|
||||
const rel = path.relative(resolvedSourceDir, file).replace(/\\/g, '/');
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
|
||||
if (['.png', '.jpg', '.jpeg'].includes(ext)) {
|
||||
const webpRel = rel.slice(0, -ext.length) + '.webp';
|
||||
if (rawRels.has(webpRel)) {
|
||||
// An optimized WebP version exists in the directory!
|
||||
// Map original reference to the WebP final upload path
|
||||
return {
|
||||
originalName: rel,
|
||||
finalName: webpRel,
|
||||
absolutePath: path.resolve(resolvedSourceDir, webpRel),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
originalName: rel,
|
||||
finalName: rel,
|
||||
absolutePath: file,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to construct the S3 URL for a final filename.
|
||||
*
|
||||
* @param finalName - The final processed name of the file.
|
||||
* @returns The public URL of the file on S3.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* getS3Url('js/main.js'); // 'https://my-bucket.s3.us-east-1.amazonaws.com/my-prefix/js/main.js'
|
||||
* ```
|
||||
*/
|
||||
const getS3Url = (finalName: string): string => {
|
||||
const normalizedKey = normalizeKey(cmd.targetPrefix, finalName);
|
||||
if (endpoint) {
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
return `${cleanEndpoint}/${bucket}/${normalizedKey}`;
|
||||
}
|
||||
return `https://${bucket}.s3.${region}.amazonaws.com/${normalizedKey}`;
|
||||
};
|
||||
|
||||
// Find workspace source files matching `src/**/*.{ts,tsx,css,md,mdx}`
|
||||
const sourceFiles = await fg('src/**/*.{ts,tsx,css,md,mdx}', {
|
||||
cwd: process.cwd(),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
// Sort assets by length descending to handle overlapping name replacement correctly
|
||||
const sortedAssets = [...processedFiles].sort(
|
||||
(a, b) => b.originalName.length - a.originalName.length
|
||||
);
|
||||
|
||||
// Replace static asset references in workspace source files
|
||||
for (const sourceFile of sourceFiles) {
|
||||
let content = await fs.promises.readFile(sourceFile, 'utf-8');
|
||||
if (!content.includes('/static/')) {
|
||||
continue;
|
||||
}
|
||||
let modified = false;
|
||||
|
||||
for (const asset of sortedAssets) {
|
||||
let searchName = asset.originalName;
|
||||
if (searchName.startsWith('static/')) {
|
||||
searchName = searchName.substring('static/'.length);
|
||||
} else if (searchName.startsWith('/static/')) {
|
||||
searchName = searchName.substring('/static/'.length);
|
||||
}
|
||||
const searchStr = `/static/${searchName}`;
|
||||
const assetUrl = getS3Url(asset.finalName);
|
||||
|
||||
if (content.includes(searchStr)) {
|
||||
content = content.split(searchStr).join(assetUrl);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
await fs.promises.writeFile(sourceFile, content, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate files for S3 upload by their S3 key (finalName)
|
||||
const uniqueUploads = Array.from(
|
||||
new Map(processedFiles.map((file) => [file.finalName, file])).values()
|
||||
);
|
||||
|
||||
// Upload files to S3
|
||||
console.log(`Syncing ${uniqueUploads.length} unique files to s3://${bucket}/${cmd.targetPrefix}...`);
|
||||
for (const file of uniqueUploads) {
|
||||
const s3Key = normalizeKey(cmd.targetPrefix, file.finalName);
|
||||
const contentType = mime.lookup(file.absolutePath) || 'application/octet-stream';
|
||||
const fileContent = await fs.promises.readFile(file.absolutePath);
|
||||
|
||||
await s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: s3Key,
|
||||
Body: fileContent,
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Base configuration interface for all CLI commands.
|
||||
*/
|
||||
export interface BaseCommandConfig {
|
||||
/** The name of the command */
|
||||
name: string;
|
||||
/** A brief description of what the command does */
|
||||
description: string;
|
||||
/** Optional array of command-line options/flags */
|
||||
options?: {
|
||||
/** The flag definitions, e.g., '-f, --force' */
|
||||
flags: string;
|
||||
/** Description of the option */
|
||||
description: string;
|
||||
/** The default value if the option is not provided */
|
||||
defaultValue?: string | boolean | string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a Bash script based command.
|
||||
*/
|
||||
export interface BashCommandConfig extends BaseCommandConfig {
|
||||
/** The bash script or command to execute */
|
||||
script: string;
|
||||
/** The type discriminator for bash commands */
|
||||
type: 'bash';
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a Node.js function based command.
|
||||
*/
|
||||
export interface NodeCommandConfig extends BaseCommandConfig {
|
||||
/** The action function to execute when the command is run */
|
||||
action: (options: Record<string, unknown>) => Promise<void> | void;
|
||||
/** The type discriminator for node commands */
|
||||
type: 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a processed file ready for upload.
|
||||
*/
|
||||
export interface ProcessedFile {
|
||||
/** The original name of the file before processing */
|
||||
originalName: string;
|
||||
/** The final name of the file after processing, used as S3 key */
|
||||
finalName: string;
|
||||
/** The absolute path of the file on the local filesystem */
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook value that can be either a shell command string or an async JS function.
|
||||
*
|
||||
* - **`string`** — executed as a shell command via the system shell.
|
||||
* - **`() => Promise<void>`** — called directly, useful for programmatic pre-processing.
|
||||
*/
|
||||
export type HookValue = string | (() => Promise<void>);
|
||||
|
||||
/**
|
||||
* Lifecycle hooks for the S3 sync command.
|
||||
*/
|
||||
export interface S3SyncHooks {
|
||||
/**
|
||||
* Runs before the sync process starts (before files are discovered).
|
||||
*
|
||||
* Accepts either a shell command string or an async function.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Shell command
|
||||
* hooks: { beforeSync: 'bash ./scripts/optimize-images.sh' }
|
||||
*
|
||||
* // JS function
|
||||
* hooks: { beforeSync: async () => { await optimizeImages(); } }
|
||||
* ```
|
||||
*/
|
||||
beforeSync?: HookValue;
|
||||
|
||||
/**
|
||||
* Runs after files are discovered but before they are uploaded.
|
||||
*
|
||||
* Receives the raw list of absolute file paths and must return the processed
|
||||
* file descriptors to upload. Useful for filtering, renaming, or replacing
|
||||
* files (e.g., swapping `.png` originals for their `.webp` counterparts).
|
||||
*
|
||||
* @param files - Absolute paths of all discovered files.
|
||||
* @returns The processed file list to upload.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* hooks: {
|
||||
* beforeUpload: async (files) =>
|
||||
* files.map((f) => ({ originalName: path.basename(f), finalName: path.basename(f), absolutePath: f }))
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
beforeUpload?: (files: string[]) => Promise<ProcessedFile[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for an S3 synchronization command.
|
||||
*/
|
||||
export interface S3SyncCommandConfig extends BaseCommandConfig {
|
||||
/** The type discriminator for S3 sync commands */
|
||||
type: 's3-sync';
|
||||
/** The source directory containing files to sync */
|
||||
sourceDir?: string;
|
||||
/** The prefix (directory) under which to upload the files in the S3 bucket */
|
||||
targetPrefix: string;
|
||||
/** Optional S3 bucket name */
|
||||
bucket?: string;
|
||||
/** Optional AWS region */
|
||||
region?: string;
|
||||
/** Optional custom endpoint URL (e.g., for MinIO or Yandex Cloud) */
|
||||
endpoint?: string;
|
||||
/** Optional AWS Access Key ID */
|
||||
accessKeyId?: string;
|
||||
/** Optional AWS Secret Access Key */
|
||||
secretAccessKey?: string;
|
||||
/** Hooks to modify the sync process at various lifecycle stages */
|
||||
hooks?: S3SyncHooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing any valid CLI command configuration.
|
||||
*/
|
||||
export type CliCommand = BashCommandConfig | NodeCommandConfig | S3SyncCommandConfig;
|
||||
|
||||
/**
|
||||
* Root configuration object for the CLI toolkit.
|
||||
*/
|
||||
export interface CliConfig {
|
||||
/** Array of registered commands */
|
||||
commands: CliCommand[];
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { baseVitestConfig } from '@ermnvldmr/rsbuild-config/dev';
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
|
||||
export default mergeConfig(baseVitestConfig, {
|
||||
test: {
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { pluginBabel } from '@rsbuild/plugin-babel';
|
||||
import { resolve } from 'path';
|
||||
import type { StorybookConfig } from 'storybook-react-rsbuild';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-themes',
|
||||
],
|
||||
framework: {
|
||||
name: 'storybook-react-rsbuild',
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
check: false,
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
|
||||
},
|
||||
},
|
||||
rsbuildFinal: (config) => {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@ermnvldmr/stl': resolve(__dirname, '../../stl/src'),
|
||||
'@ermnvldmr/ui': resolve(__dirname, '../../ui/src'),
|
||||
};
|
||||
|
||||
config.plugins = config.plugins || [];
|
||||
config.plugins.push(
|
||||
pluginBabel({
|
||||
include: /\.(?:jsx|tsx)$/,
|
||||
exclude: /[\\/]node_modules[\\/]/,
|
||||
babelLoaderOptions: (opts) => {
|
||||
opts.plugins = opts.plugins || [];
|
||||
opts.plugins.push([
|
||||
'babel-plugin-istanbul',
|
||||
{
|
||||
exclude: ['**/*.stories.tsx', '**/*.test.tsx', 'node_modules/**'],
|
||||
extension: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
]);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../../../ui/src/static/styles.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -1,48 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
class ResizeObserverMock {
|
||||
observe = vi.fn();
|
||||
unobserve = vi.fn();
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
global.ResizeObserver = ResizeObserverMock;
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // Deprecated
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// jsdom sometimes does not expose localStorage.clear(); provide a functional polyfill
|
||||
if (typeof window.localStorage.clear !== 'function') {
|
||||
const store = new Map<string, string>();
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string): string | null => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string): void => {
|
||||
store.set(key, value);
|
||||
},
|
||||
removeItem: (key: string): void => {
|
||||
store.delete(key);
|
||||
},
|
||||
clear: (): void => {
|
||||
store.clear();
|
||||
},
|
||||
key: (index: number): string | null => [...store.keys()][index] ?? null,
|
||||
get length(): number {
|
||||
return store.size;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"name": "@ermnvldmr/kits",
|
||||
"version": "1.0.0",
|
||||
"description": "Composed UI pattern assemblies and kit components",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/kits/src/index.d.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "rsbuild build --watch",
|
||||
"build": "rsbuild build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"storybook": "storybook dev -p 6008 -c .config/storybook",
|
||||
"build-storybook": "storybook build -c .config/storybook",
|
||||
"test:storybook": "test-storybook --config-dir .config/storybook",
|
||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"http-server storybook-static --port 6008 --silent\" \"wait-on tcp:127.0.0.1:6008 && test-storybook --coverage --config-dir .config/storybook --url http://127.0.0.1:6008\"",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist coverage storybook-static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ermnvldmr/i18n": "workspace:*",
|
||||
"@ermnvldmr/stl": "workspace:*",
|
||||
"@ermnvldmr/ui": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/rsbuild-config": "workspace:*",
|
||||
"@ermnvldmr/storybook-config": "workspace:*",
|
||||
"@rsbuild/core": "^1.2.11",
|
||||
"@rsbuild/plugin-babel": "^1.1.0",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
"@storybook/addon-themes": "^8.6.15",
|
||||
"@storybook/react": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@storybook/test-runner": "^0.19.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.20.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^28.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"storybook": "^8.6.17",
|
||||
"storybook-react-rsbuild": "^1.0.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.0.18",
|
||||
"wait-on": "^8.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
|
||||
|
||||
export default definePackageConfig({
|
||||
source: {
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
distPath: {
|
||||
root: 'dist',
|
||||
js: '',
|
||||
},
|
||||
filename: {
|
||||
js: '[name].js',
|
||||
},
|
||||
cleanDistPath: true,
|
||||
},
|
||||
tools: {
|
||||
rspack: {
|
||||
output: {
|
||||
library: {
|
||||
type: 'module',
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
externals: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react/jsx-runtime',
|
||||
'react/jsx-dev-runtime',
|
||||
'tailwindcss',
|
||||
'@ermnvldmr/i18n',
|
||||
'@ermnvldmr/stl',
|
||||
'@ermnvldmr/ui',
|
||||
'class-variance-authority',
|
||||
],
|
||||
externalsType: 'module',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGrid } from './BentoGrid';
|
||||
import { InfoCard } from '../InfoCard/InfoCard';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof BentoGrid> = {
|
||||
title: 'Kits/BentoGrid',
|
||||
component: BentoGrid,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof BentoGrid>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<BentoGrid>
|
||||
<InfoCard colSpan={2}>
|
||||
<InfoCard.Header>Featured Project</InfoCard.Header>
|
||||
<InfoCard.Body>This is a 2-column wide featured bento grid item.</InfoCard.Body>
|
||||
</InfoCard>
|
||||
<InfoCard colSpan={2}>
|
||||
<InfoCard.Header>Secondary Project</InfoCard.Header>
|
||||
<InfoCard.Body>Another 2-column bento card.</InfoCard.Body>
|
||||
</InfoCard>
|
||||
</BentoGrid>
|
||||
),
|
||||
};
|
||||
Loaded 100 of 404 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user