diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md index 92049a3..56190cd 100644 --- a/.kiro/steering/tech.md +++ b/.kiro/steering/tech.md @@ -102,5 +102,40 @@ No ESLint is configured for backend — the pipeline uses `node -c` syntax check | Environment | URL | Notes | |---|---|---| -| Production / Dev server | http://IP:3001 | Express serves API + static frontend build | +| Production | http://71.85.90.6:3001 | Express serves API + static frontend build | +| Staging | http://71.85.90.9:3100 | Auto-deploy on master push | | Local dev (frontend only) | http://localhost:3000 | React dev server with hot-reload, proxies API to :3001 | + +## CI/CD Pipeline + +### Infrastructure + +| Role | Host | Notes | +|---|---|---| +| GitLab instance | steam-gitlab.charterlab.com | Self-hosted GitLab | +| CI Runner (LXC 108) | 71.85.90.8 | Docker executor, Runner #6, project-locked | +| Staging target | 71.85.90.9 | Auto-deploy on master, port 3100 | +| Production target | 71.85.90.6 | Manual deploy trigger, port 3001 | + +### Executor: Docker + +The pipeline uses **Docker executor** on Runner #6. Jobs run in isolated containers: + +- **Install / Lint / Test / Build stages**: `node:18` image +- **Deploy stages**: `alpine:latest` image (installs `openssh-client` and `rsync` at runtime) + +Deploy jobs SSH from inside the Alpine container to the target hosts using a base64-encoded `$SSH_PRIVATE_KEY` stored as a GitLab CI/CD variable. + +### CI/CD Variables (project-level) + +These are set in GitLab → Settings → CI/CD → Variables: + +| Variable | Purpose | +|---|---| +| `DATABASE_URL` | PostgreSQL connection string for backend integration tests | +| `SSH_PRIVATE_KEY` | Base64-encoded private key for deploy SSH access | +| `GITLAB_PAT` | Project access token for issue comments and release creation | + +### Pipeline file + +The pipeline is defined in `.gitlab-ci.yml` at the project root. Stages: install → lint → test → build → deploy → verify. diff --git a/backend/__tests__/vendor-issue-type-dropdown.property.test.js b/backend/__tests__/vendor-issue-type-dropdown.property.test.js index b60e5a9..8ad93b1 100644 --- a/backend/__tests__/vendor-issue-type-dropdown.property.test.js +++ b/backend/__tests__/vendor-issue-type-dropdown.property.test.js @@ -15,7 +15,18 @@ const fc = require('fast-check'); // Replicate the pure functions from JiraPage.js for testing // --------------------------------------------------------------------------- -const VENDOR_PROJECT_KEYS = ['AA_VECIMA']; +const VENDOR_PROJECT_KEYS = [ + 'AA_ADTRAN', + 'AA_ADVA', + 'AA_CASA', + 'AA_CISCO', + 'AACOMMSCOP', + 'AA_COMMSCOP', + 'AA_HARMONI', + 'AA_JUNIPER', + 'AA_VECIMA', + 'AA_VIAVI', +]; const VENDOR_ISSUE_TYPES = [ 'Epic', @@ -58,22 +69,33 @@ function simulateProjectKeyChange(oldKey, newKey, currentIssueType, vendorKeys) return (wasVendor !== isNowVendor) ? '' : currentIssueType; } +// Helper: generate a vendor key with random casing and optional whitespace +const arbVendorKey = fc.constantFrom(...VENDOR_PROJECT_KEYS).chain(key => + fc.oneof( + fc.constant(key), + fc.constant(key.toLowerCase()), + fc.constant(` ${key} `), + ) +); + +// Helper: generate a string that does NOT match any vendor key after normalization +const arbNonVendorKey = fc.string({ minLength: 0, maxLength: 50 }).filter(s => { + const normalized = s.trim().toUpperCase(); + return !VENDOR_PROJECT_KEYS.includes(normalized); +}); + +const arbNonVendorKeyNonEmpty = fc.string({ minLength: 1, maxLength: 30 }).filter(s => { + const normalized = s.trim().toUpperCase(); + return !VENDOR_PROJECT_KEYS.includes(normalized) && normalized.length > 0; +}); + // --------------------------------------------------------------------------- // Property 1: Issue type list determination // --------------------------------------------------------------------------- describe('Feature: vendor-issue-type-dropdown, Property 1: Issue type list determination', () => { it('returns VENDOR_ISSUE_TYPES when project key matches a vendor key (case-insensitive, trimmed)', () => { - // Generate variations of the vendor key with different casing and whitespace - const vendorKeyVariants = fc.oneof( - fc.constant('AA_VECIMA'), - fc.constant('aa_vecima'), - fc.constant('Aa_Vecima'), - fc.constant(' AA_VECIMA '), - fc.constant('aa_VECIMA'), - ); - fc.assert( - fc.property(vendorKeyVariants, (key) => { + fc.property(arbVendorKey, (key) => { const result = getIssueTypesForProject(key, VENDOR_PROJECT_KEYS, VENDOR_ISSUE_TYPES, STEAM_ISSUE_TYPES); expect(result).toBe(VENDOR_ISSUE_TYPES); }), @@ -82,14 +104,8 @@ describe('Feature: vendor-issue-type-dropdown, Property 1: Issue type list deter }); it('returns STEAM_ISSUE_TYPES for any string that does not match a vendor key after normalization', () => { - // Generate arbitrary strings that are NOT 'AA_VECIMA' after trim+uppercase - const nonVendorKey = fc.string({ minLength: 0, maxLength: 50 }).filter(s => { - const normalized = s.trim().toUpperCase(); - return normalized !== 'AA_VECIMA'; - }); - fc.assert( - fc.property(nonVendorKey, (key) => { + fc.property(arbNonVendorKey, (key) => { const result = getIssueTypesForProject(key, VENDOR_PROJECT_KEYS, VENDOR_ISSUE_TYPES, STEAM_ISSUE_TYPES); expect(result).toBe(STEAM_ISSUE_TYPES); }), @@ -120,20 +136,10 @@ describe('Feature: vendor-issue-type-dropdown, Property 1: Issue type list deter // --------------------------------------------------------------------------- describe('Feature: vendor-issue-type-dropdown, Property 2: Context switch resets issue type', () => { it('resets issue_type to empty when switching from vendor to non-vendor context', () => { - // Generate a vendor key variant and a non-vendor key - const vendorKey = fc.oneof( - fc.constant('AA_VECIMA'), - fc.constant('aa_vecima'), - fc.constant(' AA_VECIMA '), - ); - const nonVendorKey = fc.string({ minLength: 1, maxLength: 30 }).filter(s => { - const normalized = s.trim().toUpperCase(); - return normalized !== 'AA_VECIMA' && normalized.length > 0; - }); const anyIssueType = fc.string({ minLength: 1, maxLength: 50 }); fc.assert( - fc.property(vendorKey, nonVendorKey, anyIssueType, (oldKey, newKey, issueType) => { + fc.property(arbVendorKey, arbNonVendorKeyNonEmpty, anyIssueType, (oldKey, newKey, issueType) => { const result = simulateProjectKeyChange(oldKey, newKey, issueType, VENDOR_PROJECT_KEYS); expect(result).toBe(''); }), @@ -142,18 +148,10 @@ describe('Feature: vendor-issue-type-dropdown, Property 2: Context switch resets }); it('resets issue_type to empty when switching from non-vendor to vendor context', () => { - const nonVendorKey = fc.string({ minLength: 1, maxLength: 30 }).filter(s => { - const normalized = s.trim().toUpperCase(); - return normalized !== 'AA_VECIMA' && normalized.length > 0; - }); - const vendorKey = fc.oneof( - fc.constant('AA_VECIMA'), - fc.constant('aa_vecima'), - ); const anyIssueType = fc.string({ minLength: 1, maxLength: 50 }); fc.assert( - fc.property(nonVendorKey, vendorKey, anyIssueType, (oldKey, newKey, issueType) => { + fc.property(arbNonVendorKeyNonEmpty, arbVendorKey, anyIssueType, (oldKey, newKey, issueType) => { const result = simulateProjectKeyChange(oldKey, newKey, issueType, VENDOR_PROJECT_KEYS); expect(result).toBe(''); }), @@ -167,18 +165,10 @@ describe('Feature: vendor-issue-type-dropdown, Property 2: Context switch resets // --------------------------------------------------------------------------- describe('Feature: vendor-issue-type-dropdown, Property 3: Same context preserves issue type', () => { it('preserves issue_type when both old and new keys resolve to STEAM context', () => { - const nonVendorKey1 = fc.string({ minLength: 0, maxLength: 30 }).filter(s => { - const normalized = s.trim().toUpperCase(); - return normalized !== 'AA_VECIMA'; - }); - const nonVendorKey2 = fc.string({ minLength: 0, maxLength: 30 }).filter(s => { - const normalized = s.trim().toUpperCase(); - return normalized !== 'AA_VECIMA'; - }); const anyIssueType = fc.string({ minLength: 0, maxLength: 50 }); fc.assert( - fc.property(nonVendorKey1, nonVendorKey2, anyIssueType, (oldKey, newKey, issueType) => { + fc.property(arbNonVendorKey, arbNonVendorKey, anyIssueType, (oldKey, newKey, issueType) => { const result = simulateProjectKeyChange(oldKey, newKey, issueType, VENDOR_PROJECT_KEYS); expect(result).toBe(issueType); }), @@ -187,21 +177,10 @@ describe('Feature: vendor-issue-type-dropdown, Property 3: Same context preserve }); it('preserves issue_type when both old and new keys resolve to vendor context', () => { - // With only one vendor key, both must be variants of AA_VECIMA - const vendorKey1 = fc.oneof( - fc.constant('AA_VECIMA'), - fc.constant('aa_vecima'), - fc.constant(' AA_VECIMA '), - ); - const vendorKey2 = fc.oneof( - fc.constant('AA_VECIMA'), - fc.constant('Aa_Vecima'), - fc.constant('aa_VECIMA'), - ); const anyIssueType = fc.string({ minLength: 0, maxLength: 50 }); fc.assert( - fc.property(vendorKey1, vendorKey2, anyIssueType, (oldKey, newKey, issueType) => { + fc.property(arbVendorKey, arbVendorKey, anyIssueType, (oldKey, newKey, issueType) => { const result = simulateProjectKeyChange(oldKey, newKey, issueType, VENDOR_PROJECT_KEYS); expect(result).toBe(issueType); }), diff --git a/docs/guides/full-reference-manual.md b/docs/guides/full-reference-manual.md index 7c00313..64eccc4 100644 --- a/docs/guides/full-reference-manual.md +++ b/docs/guides/full-reference-manual.md @@ -533,6 +533,8 @@ A dedicated page for managing Jira Data Center tickets linked to CVE/vendor pair - **Sync** — refresh a single ticket's status and summary from Jira, or bulk-sync all tracked tickets via JQL search - **Create / Edit / Delete** — manage local ticket records linking Jira keys to CVE/vendor pairs +**Vendor-specific issue types:** The Issue Type dropdown in the creation modal is context-aware. When the Project Key field matches a recognized vendor project key (e.g., `AA_VECIMA`, `AA_CISCO`, `AA_ADTRAN`), the dropdown switches to vendor-specific issue types (Epic, Story, Task, Defect, Production Defect/Incident Fix, New Feature, Spike, Release Candidate, Documentation). For all other project keys — including the default from `JIRA_PROJECT_KEY` — the dropdown shows STEAM issue types (Story, Epic, Program, Project, Reservation, Automation Maintenance). Matching is case-insensitive and trims whitespace. Changing the project key such that the context switches (STEAM to vendor or vice versa) resets the selected issue type. The same behavior applies when creating a Jira ticket from the Ivanti Queue. The list of recognized vendor project keys is defined in `VENDOR_PROJECT_KEYS` in `frontend/src/components/pages/JiraPage.js`. + **Connection test (Admin)** — verify Jira API credentials and connectivity from the page header. **Rate limit monitoring (Admin)** — view current burst and daily rate limit usage against Charter's posted limits (60/minute burst, 1 440/day). diff --git a/frontend/src/components/pages/JiraPage.js b/frontend/src/components/pages/JiraPage.js index 77db544..8f18948 100644 --- a/frontend/src/components/pages/JiraPage.js +++ b/frontend/src/components/pages/JiraPage.js @@ -160,7 +160,18 @@ function getStatusColor(status) { // Vendor issue type configuration // --------------------------------------------------------------------------- // Add new vendor project keys here to enable vendor-specific issue types -const VENDOR_PROJECT_KEYS = ['AA_VECIMA']; +const VENDOR_PROJECT_KEYS = [ + 'AA_ADTRAN', + 'AA_ADVA', + 'AA_CASA', + 'AA_CISCO', + 'AACOMMSCOP', + 'AA_COMMSCOP', + 'AA_HARMONI', + 'AA_JUNIPER', + 'AA_VECIMA', + 'AA_VIAVI', +]; const VENDOR_ISSUE_TYPES = [ 'Epic',