feature/pp-58-runtime-image-contract (#68)
Some checks failed
CICD Start / Sanity and Base Decision (push) Successful in 18s
Renovate Dependency Updates / Renovate Dependencies (push) Failing after 7m19s

## Summary

This PR tightens repository quality enforcement around markdown and documentation. It adds `markdownlint` to the `cicd-checks` workflow, expands pre-commit coverage so markdown files are checked repo-wide, and cleans up the PP-58 documentation set to keep it aligned with the new policy.

## What changed

- Added a `Markdownlint Check` entry to `.gitea/workflows/cicd-checks.yaml`
- Added `markdownlint` to pre-commit and widened prettier coverage to include markdown files across the repo
- Updated `README.md` to satisfy markdownlint line-length rules
- Normalized the PP-58 documentation set:
  - `docs/DEPLOYABLE_RUNTIME_CONTRACT.md`
  - `docs/adr/ADR003-deployable_runtime_image_contract.md`
  - `docs/DEVELOPMENT.md`
  - `docs/CICD_MULTI_STAGE_BUILD.md`
  - `docs/CICD_TROUBLESHOOTING_GUIDE.md`
  - `docs/SECURE_DOCKER_CICD.md`

## Validation

- `pre-commit run markdownlint --files README.md docs/DEPLOYABLE_RUNTIME_CONTRACT.md`
- `pre-commit run prettier --files README.md docs/DEPLOYABLE_RUNTIME_CONTRACT.md`
- Workflow YAML validation returned no errors

## Notes

This change does not alter application runtime behavior. It only strengthens CI and documentation quality enforcement.

Co-authored-by: copilotcoder <copilotcoder@darkhelm.org>
Reviewed-on: #68
This commit was merged in pull request #68.
This commit is contained in:
2026-06-19 17:00:57 -04:00
parent 48a37b943f
commit 9b742a5a6d
28 changed files with 733 additions and 398 deletions

18
frontend/env.d.ts vendored
View File

@@ -1,24 +1,24 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
// Vite environment variables
interface ImportMetaEnv {
readonly VITE_API_URL?: string
readonly DEV: boolean
readonly PROD: boolean
readonly VITEST: boolean
readonly VITE_API_URL?: string;
readonly DEV: boolean;
readonly PROD: boolean;
readonly VITEST: boolean;
}
interface ImportMeta {
readonly env: ImportMetaEnv
readonly env: ImportMetaEnv;
}
// Global variables for automatic validation
declare global {
var __AUTO_VALIDATE__: boolean | undefined
var __AUTO_VALIDATE__: boolean | undefined;
}

View File

@@ -1,9 +1,9 @@
import typescript from '@typescript-eslint/eslint-plugin'
import typescriptParser from '@typescript-eslint/parser'
import vue from 'eslint-plugin-vue'
import vueParser from 'vue-eslint-parser'
import jsdoc from 'eslint-plugin-jsdoc'
import tsdoc from 'eslint-plugin-tsdoc'
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import vue from 'eslint-plugin-vue';
import vueParser from 'vue-eslint-parser';
import jsdoc from 'eslint-plugin-jsdoc';
import tsdoc from 'eslint-plugin-tsdoc';
export default [
// Ignore patterns (replaces .eslintignore)
@@ -18,8 +18,8 @@ export default [
'.vscode/',
'.idea/',
'*.tmp',
'*.temp'
]
'*.temp',
],
},
// JavaScript files
@@ -27,12 +27,12 @@ export default [
files: ['**/*.{js,mjs,cjs}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
sourceType: 'module',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
},
// TypeScript files
@@ -41,12 +41,12 @@ export default [
languageOptions: {
parser: typescriptParser,
ecmaVersion: 'latest',
sourceType: 'module'
sourceType: 'module',
},
plugins: {
'@typescript-eslint': typescript,
jsdoc,
tsdoc
tsdoc,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
@@ -61,8 +61,8 @@ export default [
'jsdoc/check-param-names': 'error',
'jsdoc/check-tag-names': 'error',
'jsdoc/check-types': 'error',
'jsdoc/valid-types': 'error'
}
'jsdoc/valid-types': 'error',
},
},
// Vue files
@@ -73,14 +73,14 @@ export default [
parserOptions: {
parser: typescriptParser,
ecmaVersion: 'latest',
sourceType: 'module'
}
sourceType: 'module',
},
},
plugins: {
vue,
'@typescript-eslint': typescript,
jsdoc,
tsdoc
tsdoc,
},
rules: {
...vue.configs['vue3-essential'].rules,
@@ -96,7 +96,7 @@ export default [
'jsdoc/check-param-names': 'error',
'jsdoc/check-tag-names': 'error',
'jsdoc/check-types': 'error',
'jsdoc/valid-types': 'error'
}
}
]
'jsdoc/valid-types': 'error',
},
},
];

View File

@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test'
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
@@ -10,7 +10,9 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'playwright-results.xml' }]] : 'html',
reporter: process.env.CI
? [['list'], ['junit', { outputFile: 'playwright-results.xml' }]]
: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
@@ -30,54 +32,56 @@ export default defineConfig({
timeout: 60000,
}),
},
projects: process.env.CI ? [
// CI Environment: Only run Chromium for reliability in Docker
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
headless: true,
launchOptions: {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
// Network resilience args
'--disable-extensions',
'--disable-plugins',
'--disable-images', // Faster loading, reduce network load
'--aggressive-cache-discard',
// Increase network timeouts
'--network-quiet-timeout=10000',
'--disable-background-networking',
]
projects: process.env.CI
? [
// CI Environment: Only run Chromium for reliability in Docker
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
headless: true,
launchOptions: {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
// Network resilience args
'--disable-extensions',
'--disable-plugins',
'--disable-images', // Faster loading, reduce network load
'--aggressive-cache-discard',
// Increase network timeouts
'--network-quiet-timeout=10000',
'--disable-background-networking',
],
},
},
},
},
}
] : [
// Local Development: Run all browsers
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
}
],
]
: [
// Local Development: Run all browsers
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
webServer: {
command: 'yarn dev',
url: 'http://localhost:5173',
@@ -85,5 +89,5 @@ export default defineConfig({
timeout: process.env.CI ? 180 * 1000 : 120 * 1000, // Longer startup timeout in CI
stderr: 'pipe',
stdout: 'pipe',
}
})
},
});

View File

@@ -2,7 +2,7 @@
* End-to-end tests using Playwright
*/
import { test, expect } from '@playwright/test'
import { test, expect } from '@playwright/test';
// Helper function for network-resilient page navigation
async function navigateWithRetry(page: any, url: string, maxRetries = 3): Promise<void> {
@@ -10,54 +10,54 @@ async function navigateWithRetry(page: any, url: string, maxRetries = 3): Promis
try {
await page.goto(url, {
waitUntil: 'networkidle',
timeout: process.env.CI ? 45000 : 30000
})
return // Success
timeout: process.env.CI ? 45000 : 30000,
});
return; // Success
} catch (error) {
if (i === maxRetries - 1) throw error // Last attempt failed
console.log(`Navigation attempt ${i + 1} failed, retrying...`)
await page.waitForTimeout(2000) // Wait before retry
if (i === maxRetries - 1) throw error; // Last attempt failed
console.log(`Navigation attempt ${i + 1} failed, retrying...`);
await page.waitForTimeout(2000); // Wait before retry
}
}
}
test.describe('Plex Playlist App', () => {
test('should display app title', async ({ page }) => {
await navigateWithRetry(page, '/')
await navigateWithRetry(page, '/');
// Wait for the app to fully load with network resilience
await page.waitForSelector('h1', { timeout: 15000 })
await expect(page.locator('h1')).toContainText('Plex Playlist')
})
await page.waitForSelector('h1', { timeout: 15000 });
await expect(page.locator('h1')).toContainText('Plex Playlist');
});
test('should have welcome message', async ({ page }) => {
await navigateWithRetry(page, '/')
await navigateWithRetry(page, '/');
// Wait for the welcome message to appear with network resilience
await page.waitForSelector('p', { timeout: 15000 })
await expect(page.locator('p')).toContainText('Welcome to the Plex Playlist Manager')
})
await page.waitForSelector('p', { timeout: 15000 });
await expect(page.locator('p')).toContainText('Welcome to the Plex Playlist Manager');
});
test('should load without errors', async ({ page }) => {
const errors: string[] = []
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
// Filter out network-related errors that are acceptable in CI
const errorText = msg.text()
const errorText = msg.text();
if (!errorText.includes('net::') && !errorText.includes('Failed to fetch')) {
errors.push(errorText)
errors.push(errorText);
}
}
})
});
await navigateWithRetry(page, '/')
await navigateWithRetry(page, '/');
// Wait for app to fully load with extra time for network instability
await page.waitForLoadState('networkidle')
await page.waitForLoadState('networkidle');
// Give extra time for any async operations in unstable networks
await page.waitForTimeout(process.env.CI ? 3000 : 1000)
await page.waitForTimeout(process.env.CI ? 3000 : 1000);
expect(errors).toHaveLength(0)
})
})
expect(errors).toHaveLength(0);
});
});

View File

@@ -1,10 +1,10 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '@/App.vue'
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import App from '@/App.vue';
describe('App.vue', () => {
it('renders properly', () => {
const wrapper = mount(App)
expect(wrapper.text()).toContain('Plex Playlist')
})
})
const wrapper = mount(App);
expect(wrapper.text()).toContain('Plex Playlist');
});
});

View File

@@ -1,13 +1,7 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",

View File

@@ -1,16 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue()
],
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
host: '0.0.0.0',
@@ -19,12 +17,12 @@ export default defineConfig({
'/api': {
target: 'http://backend:8000',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, '')
}
}
rewrite: (path: string) => path.replace(/^\/api/, ''),
},
},
},
define: {
// Enable automatic validation in development
__AUTO_VALIDATE__: JSON.stringify(process.env.NODE_ENV !== 'production')
}
})
__AUTO_VALIDATE__: JSON.stringify(process.env.NODE_ENV !== 'production'),
},
});

View File

@@ -1,18 +1,21 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
globals: true,
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', 'tests/unit/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
include: [
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'tests/unit/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
],
exclude: ['tests/e2e/**/*'],
setupFiles: ['./src/test-setup.ts'], // Automatically install Zod validation hooks
coverage: {
@@ -25,16 +28,16 @@ export default defineConfig({
'**/*.d.ts',
'coverage/',
'tests/',
'playwright.config.ts'
'playwright.config.ts',
],
thresholds: {
global: {
lines: 85,
functions: 85,
branches: 85,
statements: 85
}
}
}
}
})
statements: 85,
},
},
},
},
});