From c23a0cbacc06c6672fcfc925da13bd8405d15317 Mon Sep 17 00:00:00 2001 From: Kamil Musial Date: Fri, 14 Nov 2025 12:01:24 +0100 Subject: [PATCH 1/4] fix(jira-integration): Complete GitHub <> JIRA integration fixes (DEX-36, ALL-593) --- .env.example | 68 ++ .gitignore | 1 + README.md | 69 +- event.json | 38 + package-lock.json | 1775 +++++++++++++++++++---------- package.json | 1 + update_jira/event.json | 38 + update_jira/event.local.json | 35 + update_jira/index.js | 699 ++++++++---- update_jira/index.test.js | 121 ++ utils/jira.integration.test.js | 360 ++++++ utils/test-custom-field-update.js | 214 ++++ utils/verify-custom-fields.js | 228 ++++ 13 files changed, 2829 insertions(+), 818 deletions(-) create mode 100644 .env.example create mode 100644 event.json create mode 100644 update_jira/event.json create mode 100644 update_jira/event.local.json create mode 100644 update_jira/index.test.js create mode 100644 utils/jira.integration.test.js create mode 100644 utils/test-custom-field-update.js create mode 100644 utils/verify-custom-fields.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2bf3671 --- /dev/null +++ b/.env.example @@ -0,0 +1,68 @@ +# ============================================================================== +# Jira Integration Environment Variables +# ============================================================================== +# Copy this file to .env and fill in your actual values +# +# Required for Jira API access +JIRA_BASE_URL=https://coursedog.atlassian.net +JIRA_EMAIL=your-email@domain.com +JIRA_API_TOKEN=your-jira-api-token + +# Optional: Jira project key for filtering (leave empty to search all projects) +JIRA_PROJECT_KEY= + +# ============================================================================== +# GitHub Context Variables (for local testing) +# ============================================================================== +# These are automatically set when running in GitHub Actions +# For local testing, set them manually to simulate different scenarios + +# Git reference (branch or tag) +# Examples: refs/heads/main, refs/heads/staging, refs/heads/dev +GITHUB_REF=refs/heads/staging + +# Event type that triggered the workflow +# Options: push, pull_request, pull_request_target +GITHUB_EVENT_NAME=push + +# Path to the GitHub event payload file +# For local testing, use ./update_jira/event.local.json +GITHUB_EVENT_PATH=./update_jira/event.local.json + +# Repository in owner/repo format +GITHUB_REPOSITORY=coursedog/notion-scripts + +# GitHub token for API access (required for fetching commit data) +GITHUB_TOKEN=your-github-token + +# ============================================================================== +# Testing Variables +# ============================================================================== +# For running verification and test scripts + +# Test issue key for custom field verification +TEST_JIRA_ISSUE_KEY=DEX-36 + +# Enable dry-run mode (will log actions but not actually update Jira) +# DRY_RUN=true + +# ============================================================================== +# Custom Field Configuration Reference +# ============================================================================== +# The following custom fields are used for deployment tracking (from ALL-593): +# +# customfield_11473: Release Environment (select field) +# - Option ID 11942: staging +# - Option ID 11943: production +# +# customfield_11474: Stage Release Timestamp (datetime) +# - Set automatically on staging deployments +# +# customfield_11475: Production Release Timestamp (datetime) +# - Set automatically on production deployments +# +# To verify these field IDs are correct for your Jira instance: +# node utils/verify-custom-fields.js +# +# To test updating these fields: +# node utils/test-custom-field-update.js DEX-36 diff --git a/.gitignore b/.gitignore index 96fd225..26b3e01 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ web_modules/ .yarn-integrity # dotenv environment variables file +.env .env.development.local .env.test.local .env.production.local diff --git a/README.md b/README.md index 43c1a3e..d531d29 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -# notion-scripts \ No newline at end of file +# GitHub Actions for Jira Integration + +This repository contains GitHub Actions for automating Jira issue management based on GitHub events. + +## Actions + +### update_jira + +Automatically updates Jira issues based on pull request events and deployments. + +**Features:** + +- Transitions Jira issues based on PR status and target branch +- Updates deployment metadata (environment, timestamps) for staging/production releases +- Supports dry-run mode for testing +- Handles multiple Jira issues in PR titles/descriptions + +**Configuration:** + +```yaml +- uses: ./update_jira + with: + jira-base-url: ${{ secrets.JIRA_BASE_URL }} + jira-email: ${{ secrets.JIRA_EMAIL }} + jira-api-token: ${{ secrets.JIRA_API_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + dry-run: 'false' +``` + +**Custom Fields:** + +- `customfield_11473`: Release Environment (staging/production) +- `customfield_11474`: Stage Release Timestamp +- `customfield_11475`: Production Release Timestamp + +**Local Testing:** + +1. Copy `.env.example` to `.env` and fill in credentials +2. Create `update_jira/event.local.json` with a sample GitHub event +3. Run: `node update_jira/index.js` + +**Verification Scripts:** + +- `utils/verify-custom-fields.js`: Verify custom field IDs exist in your Jira instance +- `utils/test-custom-field-update.js`: Test custom field updates with rollback + +## Development + +**Prerequisites:** + +- Node.js 16+ +- Jira account with API token +- GitHub repository access + +**Installation:** + +```bash +npm install +``` + +**Environment Variables:** + +See `.env.example` for required configuration. + +## Related Tickets + +- **DEX-36**: Fix GitHub <> JIRA integration malfunctions +- **ALL-593**: Push deployment metadata to Jira custom fields diff --git a/event.json b/event.json new file mode 100644 index 0000000..355a008 --- /dev/null +++ b/event.json @@ -0,0 +1,38 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "url": "https://api.github.com/repos/coursedog/notion-scripts/pulls/42", + "id": 987654, + "number": 42, + "state": "closed", + "locked": false, + "title": "[DEX-36] Add deployment metadata sync to Jira", + "user": { + "login": "kamio90", + "id": 12345 + }, + "body": "Implements DEX-36: Pushes deployment metadata to Jira on deploy.", + "merged": true, + "merge_commit_sha": "abcdef1234567890", + "base": { + "ref": "main" + }, + "head": { + "ref": "feature/DEX-36-jira-deploy-metadata" + } + }, + "repository": { + "id": 123456, + "name": "notion-scripts", + "full_name": "coursedog/notion-scripts", + "owner": { + "login": "coursedog", + "id": 1 + } + }, + "sender": { + "login": "kamio90", + "id": 12345 + } +} diff --git a/package-lock.json b/package-lock.json index f1e96d7..fad2bed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,53 +1,69 @@ { "name": "notion-scripts", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@actions/core": { + "packages": { + "": { + "name": "notion-scripts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.9.1", + "@actions/github": "^5.0.0", + "@notionhq/client": "^0.4.9", + "@octokit/rest": "^18.12.0", + "dotenv": "^17.2.3", + "glob": "^7.2.0" + }, + "devDependencies": { + "eslint": "^8.3.0", + "husky": "^7.0.4", + "lint-staged": "^12.1.2" + } + }, + "node_modules/@actions/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", - "requires": { + "dependencies": { "@actions/http-client": "^2.0.1", "uuid": "^8.3.2" - }, + } + }, + "node_modules/@actions/core/node_modules/@actions/http-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "dependencies": { - "@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", - "requires": { - "tunnel": "^0.0.6" - } - } + "tunnel": "^0.0.6" } }, - "@actions/github": { + "node_modules/@actions/github": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz", "integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==", - "requires": { + "dependencies": { "@actions/http-client": "^1.0.11", "@octokit/core": "^3.4.0", "@octokit/plugin-paginate-rest": "^2.13.3", "@octokit/plugin-rest-endpoint-methods": "^5.1.1" } }, - "@actions/http-client": { + "node_modules/@actions/http-client": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", - "requires": { + "dependencies": { "tunnel": "0.0.6" } }, - "@eslint/eslintrc": { + "node_modules/@eslint/eslintrc": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", "dev": true, - "requires": { + "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.0.0", @@ -57,47 +73,58 @@ "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "@humanwhocodes/config-array": { + "node_modules/@humanwhocodes/config-array": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", + "deprecated": "Use @eslint/config-array instead", "dev": true, - "requires": { + "dependencies": { "@humanwhocodes/object-schema": "^1.2.0", "debug": "^4.1.1", "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" } }, - "@humanwhocodes/object-schema": { + "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "@notionhq/client": { + "node_modules/@notionhq/client": { "version": "0.4.9", "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-0.4.9.tgz", "integrity": "sha512-GR76TrbETlXDsCTr4TotQ/XQzi05g1ydT9iJwyB+N5Brgq6rwYaJva87y90wdI+UwiWquC1DoEHWRe7zeGClUQ==", - "requires": { + "dependencies": { "@types/node-fetch": "^2.5.10", "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=12" } }, - "@octokit/auth-token": { + "node_modules/@octokit/auth-token": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "requires": { + "dependencies": { "@octokit/types": "^6.0.3" } }, - "@octokit/core": { + "node_modules/@octokit/core": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", - "requires": { + "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", "@octokit/request": "^5.6.0", @@ -107,58 +134,67 @@ "universal-user-agent": "^6.0.0" } }, - "@octokit/endpoint": { + "node_modules/@octokit/endpoint": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "requires": { + "dependencies": { "@octokit/types": "^6.0.3", "is-plain-object": "^5.0.0", "universal-user-agent": "^6.0.0" } }, - "@octokit/graphql": { + "node_modules/@octokit/graphql": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "requires": { + "dependencies": { "@octokit/request": "^5.6.0", "@octokit/types": "^6.0.3", "universal-user-agent": "^6.0.0" } }, - "@octokit/openapi-types": { + "node_modules/@octokit/openapi-types": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" }, - "@octokit/plugin-paginate-rest": { + "node_modules/@octokit/plugin-paginate-rest": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", - "requires": { + "dependencies": { "@octokit/types": "^6.34.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" } }, - "@octokit/plugin-request-log": { + "node_modules/@octokit/plugin-request-log": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==" + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } }, - "@octokit/plugin-rest-endpoint-methods": { + "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "5.13.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", - "requires": { + "dependencies": { "@octokit/types": "^6.34.0", "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" } }, - "@octokit/request": { + "node_modules/@octokit/request": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz", "integrity": "sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA==", - "requires": { + "dependencies": { "@octokit/endpoint": "^6.0.1", "@octokit/request-error": "^2.1.0", "@octokit/types": "^6.16.1", @@ -167,311 +203,419 @@ "universal-user-agent": "^6.0.0" } }, - "@octokit/request-error": { + "node_modules/@octokit/request-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "requires": { + "dependencies": { "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", "once": "^1.4.0" } }, - "@octokit/rest": { + "node_modules/@octokit/rest": { "version": "18.12.0", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", - "requires": { + "dependencies": { "@octokit/core": "^3.5.1", "@octokit/plugin-paginate-rest": "^2.16.8", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-rest-endpoint-methods": "^5.12.0" } }, - "@octokit/types": { + "node_modules/@octokit/types": { "version": "6.34.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", - "requires": { + "dependencies": { "@octokit/openapi-types": "^11.2.0" } }, - "@types/node": { + "node_modules/@types/node": { "version": "16.11.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.11.tgz", "integrity": "sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==" }, - "@types/node-fetch": { + "node_modules/@types/node-fetch": { "version": "2.5.12", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", - "requires": { + "dependencies": { "@types/node": "*", "form-data": "^3.0.0" } }, - "acorn": { + "node_modules/acorn": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "dev": true + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "acorn-jsx": { + "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "aggregate-error": { + "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, - "requires": { + "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "ajv": { + "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "requires": { + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "ansi-colors": { + "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "ansi-escapes": { + "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "requires": { + "dependencies": { "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "ansi-regex": { + "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { + "dependencies": { "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "argparse": { + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "astral-regex": { + "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "asynckit": { + "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "before-after-hook": { + "node_modules/before-after-hook": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "callsites": { + "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "chalk": { + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "clean-stack": { + "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "cli-cursor": { + "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, - "requires": { + "dependencies": { "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" } }, - "cli-truncate": { + "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", "dev": true, - "requires": { + "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "color-convert": { + "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { + "dependencies": { "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "color-name": { + "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "colorette": { + "node_modules/colorette": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "dev": true }, - "combined-stream": { + "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { + "dependencies": { "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "commander": { + "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true + "dev": true, + "engines": { + "node": ">= 12" + } }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "cross-spawn": { + "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "requires": { + "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "debug": { + "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, - "requires": { + "dependencies": { "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "deep-is": { + "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delayed-stream": { + "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } }, - "deprecation": { + "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, - "doctrine": { + "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "requires": { + "dependencies": { "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "emoji-regex": { + "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "enquirer": { + "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, - "requires": { + "dependencies": { "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" } }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "eslint": { + "node_modules/eslint": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "requires": { + "dependencies": { "@eslint/eslintrc": "^1.0.4", "@humanwhocodes/config-array": "^0.6.0", "ajv": "^6.10.0", @@ -510,88 +654,128 @@ "strip-json-comments": "^3.1.0", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "eslint-scope": { + "node_modules/eslint-scope": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, - "requires": { + "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "eslint-utils": { + "node_modules/eslint-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, - "requires": { + "dependencies": { "eslint-visitor-keys": "^2.0.0" }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" } }, - "eslint-visitor-keys": { + "node_modules/eslint-visitor-keys": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", - "dev": true + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } }, - "espree": { + "node_modules/espree": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, - "requires": { + "dependencies": { "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^3.1.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "esquery": { + "node_modules/esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" } }, - "esrecurse": { + "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "esutils": { + "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "execa": { + "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "requires": { + "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", @@ -601,273 +785,381 @@ "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "fast-deep-equal": { + "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-json-stable-stringify": { + "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "fast-levenshtein": { + "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "file-entry-cache": { + "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "requires": { + "dependencies": { "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "fill-range": { + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "flat-cache": { + "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, - "requires": { + "dependencies": { "flatted": "^3.1.0", "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "flatted": { + "node_modules/flatted": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, - "form-data": { + "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { + "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "functional-red-black-tree": { + "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "get-stream": { + "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "glob": { + "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "glob-parent": { + "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "requires": { + "dependencies": { "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "globals": { + "node_modules/globals": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, - "requires": { + "dependencies": { "type-fest": "^0.20.2" }, - "dependencies": { - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "has-flag": { + "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "human-signals": { + "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true + "dev": true, + "engines": { + "node": ">=10.17.0" + } }, - "husky": { + "node_modules/husky": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", - "dev": true + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } }, - "ignore": { + "node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 4" + } }, - "import-fresh": { + "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "requires": { + "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "imurmurhash": { + "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.19" + } }, - "indent-string": { + "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-fullwidth-code-point": { + "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "is-glob": { + "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "is-plain-object": { + "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } }, - "is-stream": { + "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "js-yaml": { + "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "requires": { + "dependencies": { "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "json-stable-stringify-without-jsonify": { + "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "levn": { + "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "lilconfig": { + "node_modules/lilconfig": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + } }, - "lint-staged": { + "node_modules/lint-staged": { "version": "12.1.2", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", "dev": true, - "requires": { + "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.16", "commander": "^8.3.0", @@ -883,21 +1175,34 @@ "supports-color": "^9.0.2", "yaml": "^1.10.2" }, - "dependencies": { - "supports-color": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", - "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", - "dev": true - } + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/supports-color": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", + "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "listr2": { + "node_modules/listr2": { "version": "3.13.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.5.tgz", "integrity": "sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==", "dev": true, - "requires": { + "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", "log-update": "^4.0.0", @@ -907,728 +1212,1008 @@ "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true } } }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "node_modules/listr2/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/listr2/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, - "requires": { - "yallist": "^4.0.0" + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "micromatch": { + "node_modules/micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, - "requires": { + "dependencies": { "braces": "^3.0.1", "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" } }, - "mime-db": { + "node_modules/mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "node_modules/mime-types": { "version": "2.1.34", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "requires": { + "dependencies": { "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" } }, - "mimic-fn": { + "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "minimatch": { + "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "ms": { + "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "natural-compare": { + "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node-fetch": { + "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { + "dependencies": { "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "normalize-path": { + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "npm-run-path": { + "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "requires": { + "dependencies": { "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "object-inspect": { + "node_modules/object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { + "dependencies": { "wrappy": "1" } }, - "onetime": { + "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, - "requires": { + "dependencies": { "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "optionator": { + "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, - "requires": { + "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" } }, - "p-map": { + "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, - "requires": { + "dependencies": { "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "parent-module": { + "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "requires": { + "dependencies": { "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } }, - "path-key": { + "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "prelude-ls": { + "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.8.0" + } }, - "progress": { + "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.4.0" + } }, - "punycode": { + "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "regexpp": { + "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } }, - "resolve-from": { + "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "restore-cursor": { + "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, - "requires": { + "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" } }, - "rfdc": { + "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "dev": true }, - "rimraf": { + "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, - "requires": { + "dependencies": { "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "rxjs": { + "node_modules/rxjs": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dev": true, - "requires": { + "dependencies": { "tslib": "~2.1.0" } }, - "semver": { + "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, - "requires": { + "dependencies": { "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "shebang-command": { + "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "requires": { + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "signal-exit": { + "node_modules/signal-exit": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, - "slice-ansi": { + "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" }, - "dependencies": { - "ansi-styles": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", - "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", - "dev": true - } + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", + "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "string-argv": { + "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.6.19" + } }, - "string-width": { + "node_modules/string-width": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.0.1.tgz", "integrity": "sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==", "dev": true, - "requires": { + "dependencies": { "emoji-regex": "^9.2.2", "is-fullwidth-code-point": "^4.0.0", "strip-ansi": "^7.0.1" }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-final-newline": { + "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "strip-json-comments": { + "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "supports-color": { + "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "text-table": { + "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "through": { + "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "tr46": { + "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, - "tslib": { + "node_modules/tslib": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", "dev": true }, - "tunnel": { + "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } }, - "type-check": { + "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "type-fest": { + "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "universal-user-agent": { + "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "uuid": { + "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } }, - "v8-compile-cache": { + "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "webidl-conversions": { + "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, - "whatwg-url": { + "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "requires": { + "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, - "which": { + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "word-wrap": { + "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "wrap-ansi": { + "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "wrappy": { + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "yaml": { + "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 6" + } } } } diff --git a/package.json b/package.json index be7c5d6..15fb117 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@actions/github": "^5.0.0", "@notionhq/client": "^0.4.9", "@octokit/rest": "^18.12.0", + "dotenv": "^17.2.3", "glob": "^7.2.0" }, "devDependencies": { diff --git a/update_jira/event.json b/update_jira/event.json new file mode 100644 index 0000000..70de43e --- /dev/null +++ b/update_jira/event.json @@ -0,0 +1,38 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "url": "https://api.github.com/repos/coursedog/notion-scripts/pulls/42", + "id": 987654, + "number": 42, + "state": "closed", + "locked": false, + "title": "[DEX-36] Add deployment metadata sync to Jira", + "user": { + "login": "kamio90", + "id": 12345 + }, + "body": "Implements DEX-36: Pushes deployment metadata to Jira on deploy.", + "merged": true, + "merge_commit_sha": "abcdef1234567890", + "base": { + "ref": "main" + }, + "head": { + "ref": "feature/ALL-593-jira-deploy-metadata" + } + }, + "repository": { + "id": 123456, + "name": "notion-scripts", + "full_name": "coursedog/notion-scripts", + "owner": { + "login": "coursedog", + "id": 1 + } + }, + "sender": { + "login": "kamio90", + "id": 12345 + } +} diff --git a/update_jira/event.local.json b/update_jira/event.local.json new file mode 100644 index 0000000..a8902d5 --- /dev/null +++ b/update_jira/event.local.json @@ -0,0 +1,35 @@ +{ + "action": "closed", + "pull_request": { + "number": 123, + "title": "DEX-36: Fix GitHub JIRA integration", + "body": "This PR fixes the integration between GitHub and JIRA.\n\nRelated issues:\n- DEX-36\n- ALL-593", + "state": "closed", + "merged": true, + "draft": false, + "base": { + "ref": "staging", + "repo": { + "name": "notion-scripts", + "full_name": "coursedog/notion-scripts" + } + }, + "head": { + "ref": "DEX-36/fix-github-jira-integrations", + "sha": "abc123def456" + }, + "user": { + "login": "developer" + } + }, + "repository": { + "name": "notion-scripts", + "full_name": "coursedog/notion-scripts", + "owner": { + "login": "coursedog" + } + }, + "sender": { + "login": "developer" + } +} diff --git a/update_jira/index.js b/update_jira/index.js index 905f4ae..9f8dd73 100644 --- a/update_jira/index.js +++ b/update_jira/index.js @@ -1,95 +1,247 @@ -const core = require('@actions/core') -const github = require('@actions/github') -const { Octokit } = require('@octokit/rest') -const Jira = require('./../utils/jira') +require("dotenv").config(); +const core = require("@actions/core"); +const github = require("@actions/github"); +const { Octokit } = require("@octokit/rest"); +const Jira = require("./../utils/jira"); +const fs = require("fs"); -const stagingReleaseEnvId = '11942' -const prodReleaseEnvId = '11943' +/** + * Mask sensitive data in logs. + * @param {object} obj - Any object to mask + * @returns {object} + */ +function maskSensitive(obj) { + if (!obj || typeof obj !== "object") return obj; + // Prefer structuredClone if available + const clone = + typeof structuredClone === "function" + ? structuredClone(obj) + : JSON.parse(JSON.stringify(obj)); + if (clone.apiToken) clone.apiToken = "***"; + if (clone.email) clone.email = "***"; + if (clone.headers?.Authorization) clone.headers.Authorization = "***"; + if (clone.JIRA_API_TOKEN) clone.JIRA_API_TOKEN = "***"; + if (clone.JIRA_EMAIL) clone.JIRA_EMAIL = "***"; + return clone; +} + +/** + * Detect environment: 'ci', 'github', or 'local'. + * @returns {'ci'|'github'|'local'} + */ +function detectEnvironment() { + if (process.env.GITHUB_ACTIONS === "true") return "github"; + if (process.env.CI === "true") return "ci"; + return "local"; +} + +const ENVIRONMENT = detectEnvironment(); +/** + * Log environment and startup info (professional, clear, masked). + */ +function logEnvSection() { + console.log("\n===================="); + console.log("Coursedog: update_jira script startup"); + console.log("Environment:", ENVIRONMENT); + console.log("GITHUB_REF:", process.env.GITHUB_REF); + console.log("GITHUB_EVENT_NAME:", process.env.GITHUB_EVENT_NAME); + console.log("GITHUB_EVENT_PATH:", process.env.GITHUB_EVENT_PATH); + console.log("GITHUB_REPOSITORY:", process.env.GITHUB_REPOSITORY); + console.log("JIRA_BASE_URL:", process.env.JIRA_BASE_URL); + console.log("JIRA_EMAIL:", process.env.JIRA_EMAIL ? "***" : undefined); + console.log("JIRA_PROJECT_KEY:", process.env.JIRA_PROJECT_KEY); + console.log("====================\n"); +} + +/** + * Professional debug logger with masking and clear formatting. + * @param {string} message + * @param {...any} args + */ +function debugLog(message, ...args) { + const safeArgs = args.map(maskSensitive); + console.log(`[DEBUG] ${message}`, ...safeArgs); +} + +logEnvSection(); + +/** + * Custom Field Configuration for Deployment Tracking + * + * These custom field IDs are defined in Jira and used to track deployment metadata. + * Reference: Ticket ALL-593 + * + * Custom Fields: + * - customfield_11473: Release Environment (select field) + * Options: staging (ID: 11942), production (ID: 11943) + * - customfield_11474: Stage Release Timestamp (datetime) + * - customfield_11475: Production Release Timestamp (datetime) + * + * To verify these IDs match your Jira instance: + * node utils/verify-custom-fields.js + * + * To test updating these fields: + * node utils/test-custom-field-update.js [ISSUE_KEY] + */ +const stagingReleaseEnvId = "11942"; // Option ID for "staging" in customfield_11473 +const prodReleaseEnvId = "11943"; // Option ID for "production" in customfield_11473 +/** + * Status mapping configuration for different branch deployments. + * Maps branch names to their corresponding Jira status and custom field updates. + * + * When code is merged/pushed to these branches: + * - master/main: Production deployment → sets Production Release Timestamp + * - staging: Staging deployment → sets Stage Release Timestamp + * - dev: Development merge → no deployment timestamps set + */ const statusMap = { - 'master': { - status: 'Done', + master: { + status: "Done", transitionFields: { - resolution: 'Done' + resolution: "Done", }, customFields: { - // prod release timestamp customfield_11475: new Date(), - customfield_11473: { id: prodReleaseEnvId } - } + customfield_11473: { id: prodReleaseEnvId }, + }, }, - 'main': { - status: 'Done', + main: { + status: "Done", transitionFields: { - resolution: 'Done' + resolution: "Done", }, customFields: { - // prod release timestamp customfield_11475: new Date(), - customfield_11473: { id: prodReleaseEnvId } - } + customfield_11473: { id: prodReleaseEnvId }, + }, }, - 'staging': { - status: 'Deployed to Staging', + staging: { + status: "Deployed to Staging", transitionFields: { - resolution: 'Done' + resolution: "Done", }, customFields: { - // staging release timestamp customfield_11474: new Date(), - customfield_11473: { id: stagingReleaseEnvId } - } + customfield_11473: { id: stagingReleaseEnvId }, + }, }, - 'dev': { - status: 'Merged', + dev: { + status: "Merged", transitionFields: { - resolution: 'Done' + resolution: "Done", }, - customFields: {} - } -} + customFields: {}, + }, +}; -run() +run(); async function run() { try { + debugLog( + "run() started. Checking event type and initializing Jira connection." + ); + debugLog( + "Rollback/dry-run mode:", + process.env.DRY_RUN === "true" ? "ENABLED" : "DISABLED" + ); const { GITHUB_REF, GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REPOSITORY, GITHUB_TOKEN, - } = process.env - - const JIRA_BASE_URL = core.getInput('JIRA_BASE_URL') - const JIRA_EMAIL = core.getInput('JIRA_EMAIL') - const JIRA_API_TOKEN = core.getInput('JIRA_API_TOKEN') - + } = process.env; + + const JIRA_BASE_URL = + core.getInput("JIRA_BASE_URL") || process.env.JIRA_BASE_URL; + const JIRA_EMAIL = core.getInput("JIRA_EMAIL") || process.env.JIRA_EMAIL; + const JIRA_API_TOKEN = + core.getInput("JIRA_API_TOKEN") || process.env.JIRA_API_TOKEN; + + debugLog("Attempting to initialize Jira utility with:", { + JIRA_BASE_URL, + JIRA_EMAIL, + JIRA_API_TOKEN: JIRA_API_TOKEN ? "***" : undefined, + }); const jiraUtil = new Jira({ baseUrl: JIRA_BASE_URL, email: JIRA_EMAIL, apiToken: JIRA_API_TOKEN, - }) + }); + debugLog("Jira utility initialized."); + + // --- EVENT PAYLOAD HANDLING --- + let eventData = null; + if (ENVIRONMENT === "local") { + // Allow local override of event payload for testing + const localEventPath = "./update_jira/event.local.json"; + if (fs.existsSync(localEventPath)) { + debugLog("Detected local event payload override:", localEventPath); + eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + } else if (GITHUB_EVENT_PATH && fs.existsSync(GITHUB_EVENT_PATH)) { + debugLog( + "Loading event payload from GITHUB_EVENT_PATH:", + GITHUB_EVENT_PATH + ); + eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, "utf8")); + } else { + debugLog("No event payload found for local run."); + } + } else if (GITHUB_EVENT_PATH && fs.existsSync(GITHUB_EVENT_PATH)) { + debugLog( + "Loading event payload from GITHUB_EVENT_PATH:", + GITHUB_EVENT_PATH + ); + eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, "utf8")); + } - if (GITHUB_EVENT_NAME === 'pull_request' || GITHUB_EVENT_NAME === 'pull_request_target') { - const eventData = require(GITHUB_EVENT_PATH) - await handlePullRequestEvent(eventData, jiraUtil, GITHUB_REPOSITORY) - return + if ( + (GITHUB_EVENT_NAME === "pull_request" || + GITHUB_EVENT_NAME === "pull_request_target") && + eventData + ) { + debugLog( + "Detected pull request event. Loaded event data:", + maskSensitive(eventData) + ); + if (process.env.DRY_RUN === "true") { + debugLog( + "DRY RUN: Would handle pull request event, skipping actual Jira update." + ); + return; + } + await handlePullRequestEvent(eventData, jiraUtil, GITHUB_REPOSITORY); + return; } const allowedBranches = [ - 'refs/heads/master', - 'refs/heads/main', - 'refs/heads/staging', - 'refs/heads/dev', - ] - - if (allowedBranches.indexOf(GITHUB_REF) !== -1) { - const branchName = GITHUB_REF.split('/').pop() - await handlePushEvent(branchName, jiraUtil, GITHUB_REPOSITORY, GITHUB_TOKEN) + "refs/heads/master", + "refs/heads/main", + "refs/heads/staging", + "refs/heads/dev", + ]; + + if (allowedBranches.includes(GITHUB_REF)) { + const branchName = GITHUB_REF.split("/").pop(); + debugLog("Detected push event for branch:", branchName); + if (process.env.DRY_RUN === "true") { + debugLog( + "DRY RUN: Would handle push event, skipping actual Jira update." + ); + return; + } + await handlePushEvent( + branchName, + jiraUtil, + GITHUB_REPOSITORY, + GITHUB_TOKEN + ); } } catch (error) { - core.setFailed(error.message) + debugLog("Error in run():", error); + core.setFailed(error.message); } } @@ -97,56 +249,71 @@ async function run() { * Prepare fields for Jira transition, converting names to IDs where needed */ async function prepareFields(fields, jiraUtil) { - const preparedFields = {} + const preparedFields = {}; for (const [fieldName, fieldValue] of Object.entries(fields)) { - if (fieldName === 'resolution' && typeof fieldValue === 'string') { + if (fieldName === "resolution" && typeof fieldValue === "string") { // Look up resolution ID by name - const resolutions = await jiraUtil.getFieldOptions('resolution') - const resolution = resolutions.find(r => r.name === fieldValue) + const resolutions = await jiraUtil.getFieldOptions("resolution"); + const resolution = resolutions.find((r) => r.name === fieldValue); if (resolution) { - preparedFields.resolution = { id: resolution.id } + preparedFields.resolution = { id: resolution.id }; } else { - console.warn(`Resolution "${fieldValue}" not found`) + console.warn(`Resolution "${fieldValue}" not found`); } - } else if (fieldName === 'priority' && typeof fieldValue === 'string') { + } else if (fieldName === "priority" && typeof fieldValue === "string") { // Look up priority ID by name - const priorities = await jiraUtil.getFieldOptions('priority') - const priority = priorities.find(p => p.name === fieldValue) + const priorities = await jiraUtil.getFieldOptions("priority"); + const priority = priorities.find((p) => p.name === fieldValue); if (priority) { - preparedFields.priority = { id: priority.id } + preparedFields.priority = { id: priority.id }; } - } else if (fieldName === 'assignee' && typeof fieldValue === 'string') { + } else if (fieldName === "assignee" && typeof fieldValue === "string") { // For assignee, you might need to look up the user // This depends on your Jira configuration - preparedFields.assignee = { name: fieldValue } + preparedFields.assignee = { name: fieldValue }; } else { // Pass through other fields as-is - preparedFields[fieldName] = fieldValue + preparedFields[fieldName] = fieldValue; } } - return preparedFields + return preparedFields; } /** * Update issue with transition and then update custom fields separately */ -async function updateIssueWithCustomFields(jiraUtil, issueKey, targetStatus, excludeStates, transitionFields, customFields) { +async function updateIssueWithCustomFields( + jiraUtil, + issueKey, + targetStatus, + excludeStates, + transitionFields, + customFields +) { try { // First, transition the issue with only transition-allowed fields - const preparedTransitionFields = await prepareFields(transitionFields, jiraUtil) - await jiraUtil.transitionIssue(issueKey, targetStatus, excludeStates, preparedTransitionFields) + const preparedTransitionFields = await prepareFields( + transitionFields, + jiraUtil + ); + await jiraUtil.transitionIssue( + issueKey, + targetStatus, + excludeStates, + preparedTransitionFields + ); // Then, if there are custom fields to update, update them separately if (customFields && Object.keys(customFields).length > 0) { - await jiraUtil.updateCustomFields(issueKey, customFields) + await jiraUtil.updateCustomFields(issueKey, customFields); } - return true + return true; } catch (error) { - console.error(`Failed to update ${issueKey}:`, error.message) - throw error + console.error(`Failed to update ${issueKey}:`, error.message); + throw error; } } @@ -154,54 +321,54 @@ async function updateIssueWithCustomFields(jiraUtil, issueKey, targetStatus, exc * Handle pull request events (open, close, etc) */ async function handlePullRequestEvent(eventData, jiraUtil) { - const { action, pull_request } = eventData + const { action, pull_request } = eventData; - const issueKeys = extractJiraIssueKeys(pull_request) + const issueKeys = extractJiraIssueKeys(pull_request); if (issueKeys.length === 0) { - console.log('No Jira issue keys found in PR') - return + console.log("No Jira issue keys found in PR"); + return; } - console.log(`Found Jira issues: ${issueKeys.join(', ')}`) + console.log(`Found Jira issues: ${issueKeys.join(", ")}`); - let targetStatus = null - let transitionFields = {} - let customFields = {} - const targetBranch = pull_request.base.ref + let targetStatus = null; + let transitionFields = {}; + let customFields = {}; + const targetBranch = pull_request.base.ref; switch (action) { - case 'opened': - case 'reopened': - case 'ready_for_review': - targetStatus = 'Code Review' - break - case 'converted_to_draft': - targetStatus = 'In Development' - break - case 'synchronize': + case "opened": + case "reopened": + case "ready_for_review": + targetStatus = "Code Review"; + break; + case "converted_to_draft": + targetStatus = "In Development"; + break; + case "synchronize": if (!pull_request.draft) { - targetStatus = 'Code Review' + targetStatus = "Code Review"; } - break - case 'closed': + break; + case "closed": if (pull_request.merged) { - const branchConfig = statusMap[targetBranch] + const branchConfig = statusMap[targetBranch]; if (branchConfig) { - targetStatus = branchConfig.status - transitionFields = branchConfig.transitionFields || {} - customFields = branchConfig.customFields || {} + targetStatus = branchConfig.status; + transitionFields = branchConfig.transitionFields || {}; + customFields = branchConfig.customFields || {}; } else { - targetStatus = 'Done' - transitionFields = { resolution: 'Done' } + targetStatus = "Done"; + transitionFields = { resolution: "Done" }; } } else { - console.log('PR closed without merging, skipping status update') - return + console.log("PR closed without merging, skipping status update"); + return; } - break + break; default: - console.log('No status updates for action:', action) - break + console.log("No status updates for action:", action); + break; } if (targetStatus) { @@ -211,12 +378,12 @@ async function handlePullRequestEvent(eventData, jiraUtil) { jiraUtil, issueKey, targetStatus, - ['Blocked', 'Rejected'], + ["Blocked", "Rejected"], transitionFields, customFields - ) + ); } catch (error) { - console.error(`Failed to update ${issueKey}:`, error.message) + console.error(`Failed to update ${issueKey}:`, error.message); } } } @@ -225,144 +392,200 @@ async function handlePullRequestEvent(eventData, jiraUtil) { /** * Handle push events to branches */ -async function handlePushEvent(branch, jiraUtil, githubRepository, githubToken) { +async function handlePushEvent( + branch, + jiraUtil, + githubRepository, + githubToken +) { const octokit = new Octokit({ auth: githubToken, - }) + }); - const [githubOwner, repositoryName] = githubRepository.split('/') + const [githubOwner, repositoryName] = githubRepository.split("/"); const { data } = await octokit.rest.repos.getCommit({ owner: githubOwner, repo: repositoryName, ref: branch, perPage: 1, page: 1, - }) + }); - const { commit: { message: commitMessage } } = data - const branchConfig = statusMap[branch] + const { + commit: { message: commitMessage }, + } = data; + const branchConfig = statusMap[branch]; if (!branchConfig) { - console.log(`No status mapping for branch: ${branch}`) - return + console.log(`No status mapping for branch: ${branch}`); + return; } - const newStatus = branchConfig.status - const transitionFields = branchConfig.transitionFields || {} - const customFields = branchConfig.customFields || {} + const newStatus = branchConfig.status; + const transitionFields = branchConfig.transitionFields || {}; + const customFields = branchConfig.customFields || {}; - const shouldCheckCommitHistory = ['master', 'main', 'staging'].includes(branch) + const shouldCheckCommitHistory = ["master", "main", "staging"].includes( + branch + ); - const prMatch = commitMessage.match(/#([0-9]+)/) + const prMatch = commitMessage.match(/#([0-9]+)/); // Handle staging to production deployment - if ((branch === 'master' || branch === 'main')) { - console.log('Production deployment: extracting issues from commit history') + if (branch === "master" || branch === "main") { + console.log("Production deployment: extracting issues from commit history"); try { - const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory('HEAD~100', 'HEAD') + const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory( + "HEAD~100", + "HEAD" + ); if (commitHistoryIssues.length > 0) { - console.log(`Found ${commitHistoryIssues.length} issues in production commit history`) - - const updateResults = await updateIssuesFromCommitHistoryWithCustomFields( - jiraUtil, - commitHistoryIssues, - newStatus, - ['Blocked', 'Rejected'], - transitionFields, - customFields - ) - - console.log(`Production deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed`) + console.log( + `Found ${commitHistoryIssues.length} issues in production commit history` + ); + + const updateResults = + await updateIssuesFromCommitHistoryWithCustomFields( + jiraUtil, + commitHistoryIssues, + newStatus, + ["Blocked", "Rejected"], + transitionFields, + customFields + ); + + console.log( + `Production deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed` + ); } else { - console.log('No Jira issues found in production commit history') + console.log("No Jira issues found in production commit history"); } } catch (error) { - console.error('Error processing production commit history:', error.message) + console.error( + "Error processing production commit history:", + error.message + ); } // Also handle direct PR merges to production if (prMatch) { - const prNumber = extractPrNumber(commitMessage) - const prUrl = `${repositoryName}/pull/${prNumber}` + const prNumber = extractPrNumber(commitMessage); + const prUrl = `${repositoryName}/pull/${prNumber}`; if (prNumber) { - console.log(`Also updating issues from PR ${prUrl} to production status`) - await updateByPRWithCustomFields(jiraUtil, prUrl, newStatus, transitionFields, customFields) + console.log( + `Also updating issues from PR ${prUrl} to production status` + ); + await updateByPRWithCustomFields( + jiraUtil, + prUrl, + newStatus, + transitionFields, + customFields + ); } } - return + return; } // Handle dev to staging deployment - if (branch === 'staging') { - console.log('Staging deployment: extracting issues from commit history') + if (branch === "staging") { + console.log("Staging deployment: extracting issues from commit history"); try { // Get issue keys from commit history - const commitHistoryIssues = await jiraUtil.extractIssueKeysFromGitHubContext(github.context) + const commitHistoryIssues = + await jiraUtil.extractIssueKeysFromGitHubContext(github.context); if (commitHistoryIssues.length > 0) { - console.log(`Found ${commitHistoryIssues.length} issues in staging commit history`) + console.log( + `Found ${commitHistoryIssues.length} issues in staging commit history` + ); // Update issues found in commit history - const updateResults = await updateIssuesFromCommitHistoryWithCustomFields( - jiraUtil, - commitHistoryIssues, - newStatus, - ['Blocked', 'Rejected'], - transitionFields, - customFields - ) - - console.log(`Staging deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed`) - return + const updateResults = + await updateIssuesFromCommitHistoryWithCustomFields( + jiraUtil, + commitHistoryIssues, + newStatus, + ["Blocked", "Rejected"], + transitionFields, + customFields + ); + + console.log( + `Staging deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed` + ); + return; } else { - console.log('No Jira issues found in staging commit history') - return + console.log("No Jira issues found in staging commit history"); + return; } } catch (error) { - console.error('Error processing staging commit history:', error.message) + console.error("Error processing staging commit history:", error.message); } // Also handle direct PR merges to staging if (prMatch) { - const prNumber = prMatch[1] - const prUrl = `${repositoryName}/pull/${prNumber}` - console.log(`Also updating issues from PR ${prUrl} to staging status`) - await updateByPRWithCustomFields(jiraUtil, prUrl, newStatus, transitionFields, customFields) + const prNumber = prMatch[1]; + const prUrl = `${repositoryName}/pull/${prNumber}`; + console.log(`Also updating issues from PR ${prUrl} to staging status`); + await updateByPRWithCustomFields( + jiraUtil, + prUrl, + newStatus, + transitionFields, + customFields + ); } - return + return; } // Handle PR merges to other branches (like dev) if (prMatch) { - const prNumber = prMatch[1] - const prUrl = `${repositoryName}/pull/${prNumber}` - console.log(`Updating issues mentioning PR ${prUrl} to status: ${newStatus}`) - await updateByPRWithCustomFields(jiraUtil, prUrl, newStatus, transitionFields, customFields) + const prNumber = prMatch[1]; + const prUrl = `${repositoryName}/pull/${prNumber}`; + console.log( + `Updating issues mentioning PR ${prUrl} to status: ${newStatus}` + ); + await updateByPRWithCustomFields( + jiraUtil, + prUrl, + newStatus, + transitionFields, + customFields + ); } // Additionally, for important branches, check commit history for issue keys if (shouldCheckCommitHistory) { try { // Get issue keys from recent commit history (last 50 commits) - const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory('HEAD~50', 'HEAD') + const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory( + "HEAD~50", + "HEAD" + ); if (commitHistoryIssues.length > 0) { - console.log(`Found ${commitHistoryIssues.length} additional issues in commit history for ${branch} branch`) + console.log( + `Found ${commitHistoryIssues.length} additional issues in commit history for ${branch} branch` + ); // Update issues found in commit history - const updateResults = await updateIssuesFromCommitHistoryWithCustomFields( - jiraUtil, - commitHistoryIssues, - newStatus, - ['Blocked', 'Rejected'], - transitionFields, - customFields - ) - - console.log(`Commit history update results: ${updateResults.successful} successful, ${updateResults.failed} failed`) + const updateResults = + await updateIssuesFromCommitHistoryWithCustomFields( + jiraUtil, + commitHistoryIssues, + newStatus, + ["Blocked", "Rejected"], + transitionFields, + customFields + ); + + console.log( + `Commit history update results: ${updateResults.successful} successful, ${updateResults.failed} failed` + ); } } catch (error) { - console.error('Error processing commit history:', error.message) + console.error("Error processing commit history:", error.message); // Don't fail the entire action if commit history processing fails } } @@ -371,70 +594,96 @@ async function handlePushEvent(branch, jiraUtil, githubRepository, githubToken) /** * Update issues from commit history with separate custom field updates */ -async function updateIssuesFromCommitHistoryWithCustomFields(jiraUtil, issueKeys, targetStatus, excludeStates, transitionFields, customFields) { +async function updateIssuesFromCommitHistoryWithCustomFields( + jiraUtil, + issueKeys, + targetStatus, + excludeStates, + transitionFields, + customFields +) { if (!issueKeys || issueKeys.length === 0) { - console.log('No issue keys provided for update') - return { successful: 0, failed: 0, errors: [] } + console.log("No issue keys provided for update"); + return { successful: 0, failed: 0, errors: [] }; } - console.log(`Updating ${issueKeys.length} issues to status: ${targetStatus}`) + console.log(`Updating ${issueKeys.length} issues to status: ${targetStatus}`); const results = await Promise.allSettled( - issueKeys.map(issueKey => - updateIssueWithCustomFields(jiraUtil, issueKey, targetStatus, excludeStates, transitionFields, customFields) + issueKeys.map((issueKey) => + updateIssueWithCustomFields( + jiraUtil, + issueKey, + targetStatus, + excludeStates, + transitionFields, + customFields + ) ) - ) - - const successful = results.filter(result => result.status === 'fulfilled').length - const failed = results.filter(result => result.status === 'rejected') - const errors = failed.map(result => result.reason?.message || 'Unknown error') - - console.log(`Update summary: ${successful} successful, ${failed.length} failed`) + ); + + const successful = results.filter( + (result) => result.status === "fulfilled" + ).length; + const failed = results.filter((result) => result.status === "rejected"); + const errors = failed.map( + (result) => result.reason?.message || "Unknown error" + ); + + console.log( + `Update summary: ${successful} successful, ${failed.length} failed` + ); if (failed.length > 0) { - console.log('Failed updates:', errors) + console.log("Failed updates:", errors); } return { successful, failed: failed.length, - errors - } + errors, + }; } /** * Update issues by PR with separate custom field updates */ -async function updateByPRWithCustomFields(jiraUtil, prUrl, newStatus, transitionFields, customFields) { +async function updateByPRWithCustomFields( + jiraUtil, + prUrl, + newStatus, + transitionFields, + customFields +) { try { - let jql = `text ~ "${prUrl}"` - const response = await jiraUtil.request('/search/jql', { - method: 'POST', + const jql = `text ~ "${prUrl}"`; + const response = await jiraUtil.request("/search/jql", { + method: "POST", body: JSON.stringify({ jql, - fields: ['key', 'summary', 'status', 'description'], - maxResults: 50 - }) - }) + fields: ["key", "summary", "status", "description"], + maxResults: 50, + }), + }); - const data = await response.json() - const issues = data.issues - console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`) + const data = await response.json(); + const issues = data.issues; + console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`); for (const issue of issues) { await updateIssueWithCustomFields( jiraUtil, issue.key, newStatus, - ['Blocked', 'Rejected'], + ["Blocked", "Rejected"], transitionFields, customFields - ) + ); } - return issues.length + return issues.length; } catch (error) { - console.error(`Error updating issues by PR:`, error.message) - throw error + console.error(`Error updating issues by PR:`, error.message); + throw error; } } @@ -444,17 +693,17 @@ async function updateByPRWithCustomFields(jiraUtil, prUrl, newStatus, transition * @returns {Array} Array of Jira issue keys */ function extractJiraIssueKeys(pullRequest) { - const jiraKeyPattern = /[A-Z]+-[0-9]+/g - const keys = new Set() + const jiraKeyPattern = /[A-Z]+-[0-9]+/g; + const keys = new Set(); if (pullRequest.title) { - const titleMatches = pullRequest.title.match(jiraKeyPattern) + const titleMatches = pullRequest.title.match(jiraKeyPattern); if (titleMatches) { - titleMatches.forEach(key => keys.add(key)) + titleMatches.forEach((key) => keys.add(key)); } } - return Array.from(keys) + return Array.from(keys); } /** @@ -463,6 +712,12 @@ function extractJiraIssueKeys(pullRequest) { * @returns {string|null} PR number or null if not found */ function extractPrNumber(commitMessage) { - const prMatch = commitMessage.match(/#([0-9]+)/) - return prMatch ? prMatch[1] : null + const prMatch = commitMessage.match(/#([0-9]+)/); + return prMatch ? prMatch[1] : null; } + +// Export helpers for testability +module.exports = Object.assign(module.exports || {}, { + maskSensitive, + detectEnvironment, +}); diff --git a/update_jira/index.test.js b/update_jira/index.test.js new file mode 100644 index 0000000..e71e6db --- /dev/null +++ b/update_jira/index.test.js @@ -0,0 +1,121 @@ +/** + * Pure Node.js test suite for update_jira/index.js helpers. + * Run: node update_jira/index.test.js + */ +const assert = require("assert"); +const fs = require("fs"); +const { maskSensitive, detectEnvironment } = require("./index"); + +function test(title, fn) { + try { + fn(); + console.log(`PASS: ${title}`); + } catch (e) { + console.error(`FAIL: ${title}\n ${e.stack}`); + process.exitCode = 1; + } +} + +// --- maskSensitive --- +test("maskSensitive masks apiToken, email, headers.Authorization, JIRA_API_TOKEN, JIRA_EMAIL", () => { + const input = { + apiToken: "secret", + email: "mail", + headers: { Authorization: "tok" }, + JIRA_API_TOKEN: "tok2", + JIRA_EMAIL: "mail2", + other: "ok", + }; + const masked = maskSensitive(input); + assert.strictEqual(masked.apiToken, "***"); + assert.strictEqual(masked.email, "***"); + assert.strictEqual(masked.headers.Authorization, "***"); + assert.strictEqual(masked.JIRA_API_TOKEN, "***"); + assert.strictEqual(masked.JIRA_EMAIL, "***"); + assert.strictEqual(masked.other, "ok"); +}); +test("maskSensitive returns non-object as is", () => { + assert.strictEqual(maskSensitive(null), null); + assert.strictEqual(maskSensitive(123), 123); +}); + +// --- detectEnvironment --- +test("detectEnvironment detects github", () => { + const old = process.env.GITHUB_ACTIONS; + process.env.GITHUB_ACTIONS = "true"; + assert.strictEqual(detectEnvironment(), "github"); + process.env.GITHUB_ACTIONS = old; +}); +test("detectEnvironment detects ci", () => { + const old1 = process.env.GITHUB_ACTIONS, + old2 = process.env.CI; + delete process.env.GITHUB_ACTIONS; + process.env.CI = "true"; + assert.strictEqual(detectEnvironment(), "ci"); + process.env.GITHUB_ACTIONS = old1; + process.env.CI = old2; +}); +test("detectEnvironment detects local", () => { + const old1 = process.env.GITHUB_ACTIONS, + old2 = process.env.CI; + delete process.env.GITHUB_ACTIONS; + delete process.env.CI; + assert.strictEqual(detectEnvironment(), "local"); + process.env.GITHUB_ACTIONS = old1; + process.env.CI = old2; +}); + +// --- Event payload loading logic (mocked) --- +test("loads event.local.json if present (local env)", () => { + // Mock fs.existsSync and fs.readFileSync + const existsSyncOrig = fs.existsSync, + readFileSyncOrig = fs.readFileSync; + fs.existsSync = (p) => p.includes("event.local.json"); + fs.readFileSync = () => '{"foo":42}'; + let eventData = null; + const localEventPath = "./update_jira/event.local.json"; + if (fs.existsSync(localEventPath)) { + eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + } + assert.deepStrictEqual(eventData, { foo: 42 }); + fs.existsSync = existsSyncOrig; + fs.readFileSync = readFileSyncOrig; +}); +test("loads GITHUB_EVENT_PATH if event.local.json not present (local env)", () => { + const existsSyncOrig = fs.existsSync, + readFileSyncOrig = fs.readFileSync; + process.env.GITHUB_EVENT_PATH = "/fake/path/event.json"; + fs.existsSync = (p) => p === "/fake/path/event.json"; + fs.readFileSync = () => '{"bar":99}'; + let eventData = null; + const localEventPath = "./update_jira/event.local.json"; + if (fs.existsSync(localEventPath)) { + eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + } else if ( + process.env.GITHUB_EVENT_PATH && + fs.existsSync(process.env.GITHUB_EVENT_PATH) + ) { + eventData = JSON.parse( + fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8") + ); + } + assert.deepStrictEqual(eventData, { bar: 99 }); + fs.existsSync = existsSyncOrig; + fs.readFileSync = readFileSyncOrig; +}); + +// --- Dry-run mode logic --- +test("dry-run mode skips update logic", () => { + process.env.DRY_RUN = "true"; + let called = false; + function mockJiraUpdate() { + called = true; + } + if (process.env.DRY_RUN === "true") { + // Should not call mockJiraUpdate + } else { + mockJiraUpdate(); + } + assert.strictEqual(called, false); + delete process.env.DRY_RUN; +}); diff --git a/utils/jira.integration.test.js b/utils/jira.integration.test.js new file mode 100644 index 0000000..72b91c6 --- /dev/null +++ b/utils/jira.integration.test.js @@ -0,0 +1,360 @@ +/** + * Jira Integration Test Suite + * + * Comprehensive integration tests for the Jira utility class. + * Intended for local/manual runs (not CI unit tests) and covers real API calls. + * + * Usage: + * node utils/jira.integration.test.js + * + * NOTE: Requires valid .env configuration with Jira credentials and test issue/project keys. + * + * At the end, you can revert all changes made by this test by answering 'yes' to the prompt. + */ + +require("dotenv").config(); +const Jira = require("./jira"); + +/** + * Mask sensitive data in logs. + * @param {object} obj + * @returns {object} + */ +function maskSensitive(obj) { + const clone = + typeof structuredClone === "function" + ? structuredClone(obj) + : JSON.parse(JSON.stringify(obj)); + if (clone.apiToken) clone.apiToken = "***"; + if (clone.email) clone.email = "***"; + if (clone.headers?.Authorization) clone.headers.Authorization = "***"; + return clone; +} + +/** + * Log a section header for test output. + * @param {string} title + */ +function logSection(title) { + console.log("\n===================="); + console.log(title); + console.log("===================="); +} + +/** + * Capture the original state of the test issue for rollback. + * @param {Jira} jira + * @param {string} issueKey + * @param {string} customField + * @returns {Promise<{status: string, customFieldValue: any}>} + */ +async function captureOriginalIssueState(jira, issueKey, customField) { + logSection("Capture original issue state for rollback"); + try { + const issueResp = await jira.request( + `/issue/${issueKey}?fields=status,${customField}` + ); + const issueData = await issueResp.json(); + const status = issueData.fields.status.name; + const customFieldValue = issueData.fields[customField]; + console.log(`Original status: ${status}`); + console.log(`Original custom field (${customField}):`, customFieldValue); + return { status, customFieldValue }; + } catch (err) { + console.error("Failed to capture original state:", err.message); + return { status: null, customFieldValue: null }; + } +} + +/** + * Rollback the test issue to its original state. + * @param {Jira} jira + * @param {string} issueKey + * @param {string} customField + * @param {any} originalCustomFieldValue + * @param {string} originalStatus + * @returns {Promise} + */ +async function rollbackIssueState( + jira, + issueKey, + customField, + originalCustomFieldValue, + originalStatus +) { + logSection("ROLLBACK: Reverting all changes made by this test..."); + let rollbackErrors = false; + try { + await jira.updateCustomField( + issueKey, + customField, + originalCustomFieldValue + ); + console.log( + `Rolled back custom field ${customField} to:`, + originalCustomFieldValue + ); + } catch (err) { + console.error("Failed to rollback custom field:", err.message); + rollbackErrors = true; + } + try { + const issueResp = await jira.request(`/issue/${issueKey}?fields=status`); + const issueData = await issueResp.json(); + const currentStatus = issueData.fields.status.name; + if (originalStatus && currentStatus !== originalStatus) { + await jira.transitionIssue(issueKey, originalStatus); + console.log(`Rolled back status to: ${originalStatus}`); + } else { + console.log("No status rollback needed."); + } + } catch (err) { + console.error("Failed to rollback status:", err.message); + rollbackErrors = true; + } + if (rollbackErrors) { + console.log("Rollback completed with errors. Check logs above."); + } else { + console.log("Rollback completed successfully."); + } +} + +/** + * Main test runner for Jira integration tests. + * Runs all test cases and handles rollback prompt. + * @returns {Promise} + */ + +// Main test runner (top-level await for ESLint compliance) +const jira = new Jira({ + baseUrl: process.env.JIRA_BASE_URL, + email: process.env.JIRA_EMAIL, + apiToken: process.env.JIRA_API_TOKEN, +}); + +logSection("Jira instance created"); +console.dir(maskSensitive(jira), { depth: 1 }); + +// Test configuration +const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || "DEX-36"; +const testProjectKey = process.env.TEST_JIRA_PROJECT_KEY || "DEX"; +const testCustomField = + process.env.TEST_JIRA_CUSTOM_FIELD || "customfield_10001"; +const testCustomValue = process.env.TEST_JIRA_CUSTOM_VALUE || "test-value"; +const testStatus = process.env.TEST_JIRA_STATUS || "Done"; +const testPRUrl = + process.env.TEST_JIRA_PR_URL || + "https://github.com/coursedog/notion-scripts/pull/42"; + +// --- CAPTURE ORIGINAL STATE --- +const { status: originalStatus, customFieldValue: originalCustomFieldValue } = + await captureOriginalIssueState(jira, testIssueKey, testCustomField); + +// --- TEST CASES --- +try { + logSection("Test: List all workflows"); + const workflows = await jira.getAllWorkflows(); + console.log( + "Workflows:", + workflows.map((w) => w.name || w.id) + ); +} catch (err) { + console.error("getAllWorkflows error:", err.message); +} + +try { + logSection("Test: Get project workflow name"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + console.log("Workflow name for project", testProjectKey, ":", wfName); +} catch (err) { + console.error("getProjectWorkflowName error:", err.message); +} + +try { + logSection("Test: Get workflow state machine"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + const sm = await jira.getWorkflowStateMachine(wfName); + console.log("State machine states:", Object.keys(sm.states)); +} catch (err) { + console.error("getWorkflowStateMachine error:", err.message); +} + +try { + logSection("Test: Get available transitions for issue"); + const transitions = await jira.getTransitions(testIssueKey); + console.log( + "Transitions:", + transitions.map((t) => `${t.name} → ${t.to.name}`) + ); +} catch (err) { + console.error("getTransitions error:", err.message); +} + +try { + logSection("Test: Find issues by status"); + const issues = await jira.findByStatus(testStatus); + console.log( + "Issues in status", + testStatus, + ":", + issues.map((i) => i.key) + ); +} catch (err) { + console.error("findByStatus error:", err.message); +} + +try { + logSection("Test: List all statuses"); + const statuses = await jira.getAllStatuses(); + console.log( + "Statuses:", + statuses.map((s) => s.name) + ); +} catch (err) { + console.error("getAllStatuses error:", err.message); +} + +try { + logSection('Test: Get field options for "resolution"'); + const options = await jira.getFieldOptions("resolution"); + console.log( + "Resolution options:", + options.map((o) => o.name) + ); +} catch (err) { + console.error("getFieldOptions error:", err.message); +} + +try { + logSection("Test: Get workflow schema"); + const schema = await jira.getWorkflowSchema(testProjectKey); + console.log("Workflow schema:", schema); +} catch (err) { + console.error("getWorkflowSchema error:", err.message); +} + +try { + logSection("Test: Update custom field (may fail if value is invalid)"); + const res = await jira.updateCustomField( + testIssueKey, + testCustomField, + testCustomValue + ); + console.log("updateCustomField result:", res); +} catch (err) { + console.error("updateCustomField error:", err.message); +} + +try { + logSection("Test: Get custom field value"); + const val = await jira.getCustomField(testIssueKey, testCustomField); + console.log("Custom field value:", val); +} catch (err) { + console.error("getCustomField error:", err.message); +} + +try { + logSection( + "Test: Update multiple custom fields (may fail if value is invalid)" + ); + const res = await jira.updateCustomFields(testIssueKey, { + [testCustomField]: testCustomValue, + }); + console.log("updateCustomFields result:", res); +} catch (err) { + console.error("updateCustomFields error:", err.message); +} + +try { + logSection("Test: Get transition details for first available transition"); + const transitions = await jira.getTransitions(testIssueKey); + if (transitions && transitions.length > 0) { + const details = await jira.getTransitionDetails( + testIssueKey, + transitions[0].id + ); + console.log("Transition details:", details); + } else { + console.log("No transitions to get details for"); + } +} catch (err) { + console.error("getTransitionDetails error:", err.message); +} + +try { + logSection("Test: Extract issue keys from commit messages"); + const keys = jira.extractIssueKeysFromCommitMessages([ + "DEX-36: test commit", + "ALL-123: another commit", + "no key here", + ]); + console.log("Extracted keys:", keys); +} catch (err) { + console.error("extractIssueKeysFromCommitMessages error:", err.message); +} + +try { + logSection("Test: Find all/shortest transition paths"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + const sm = await jira.getWorkflowStateMachine(wfName); + const allPaths = jira.findAllTransitionPaths(sm, "To Do", testStatus); + const shortest = jira.findShortestTransitionPath(sm, "To Do", testStatus); + console.log("All paths count:", allPaths.length); + console.log("Shortest path:", shortest); +} catch (err) { + console.error( + "findShortestTransitionPath/findAllTransitionPaths error:", + err.message + ); +} + +try { + logSection( + "Test: Update issues by status (may fail if workflow transition not allowed)" + ); + await jira.updateByStatus(testStatus, "In Progress", {}); +} catch (err) { + console.error("updateByStatus error:", err.message); +} + +try { + logSection("Test: Update issues by PR URL"); + await jira.updateByPR(testPRUrl, testStatus, {}); +} catch (err) { + console.error("updateByPR error:", err.message); +} + +try { + logSection("Test: Update issues from commit history"); + await jira.updateIssuesFromCommitHistory(["DEX-36", "ALL-123"], testStatus); +} catch (err) { + console.error("updateIssuesFromCommitHistory error:", err.message); +} + +try { + logSection("Test: Transition issue to target status"); + await jira.transitionIssue(testIssueKey, testStatus); +} catch (err) { + console.error("transitionIssue error:", err.message); +} + +// --- END OF TEST CASES --- + +logSection("TEST COMPLETE"); +console.log("Do you want to revert all changes made by this test? (yes/no)"); +process.stdin.setEncoding("utf8"); +process.stdin.once("data", async (data) => { + if (data.trim().toLowerCase() === "yes") { + await rollbackIssueState( + jira, + testIssueKey, + testCustomField, + originalCustomFieldValue, + originalStatus + ); + process.exit(0); + } else { + console.log("No revert performed. All changes made by this test remain."); + process.exit(0); + } +}); diff --git a/utils/test-custom-field-update.js b/utils/test-custom-field-update.js new file mode 100644 index 0000000..d914797 --- /dev/null +++ b/utils/test-custom-field-update.js @@ -0,0 +1,214 @@ +/** + * Custom Field Update Test Script + * + * Tests updating the custom fields on a real Jira issue to verify + * that the field IDs and option IDs are correct. + * + * Usage: + * node utils/test-custom-field-update.js [ISSUE_KEY] + * + * Example: + * node utils/test-custom-field-update.js DEX-36 + */ + +require('dotenv').config() +const Jira = require('./jira') + +async function testCustomFieldUpdate () { + console.log(`\n${'='.repeat(70)}`) + console.log('JIRA CUSTOM FIELD UPDATE TEST') + console.log(`${'='.repeat(70)}\n`) + + // Check environment variables + if ( + !process.env.JIRA_BASE_URL || + !process.env.JIRA_EMAIL || + !process.env.JIRA_API_TOKEN + ) { + console.error('❌ ERROR: Missing required environment variables') + console.error(' Required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN\n') + process.exit(1) + } + + const testIssueKey = + process.argv[2] || process.env.TEST_JIRA_ISSUE_KEY || 'DEX-36' + + console.log(`Test Issue: ${testIssueKey}`) + console.log(`Base URL: ${process.env.JIRA_BASE_URL}\n`) + + const jira = new Jira({ + baseUrl: process.env.JIRA_BASE_URL, + email: process.env.JIRA_EMAIL, + apiToken: process.env.JIRA_API_TOKEN, + }) + + try { + // Capture original state + console.log('─'.repeat(70)) + console.log('STEP 1: Capturing original field values') + console.log(`${'─'.repeat(70)}\n`) + + const originalResponse = await jira.request( + `/issue/${testIssueKey}?fields=customfield_11473,customfield_11474,customfield_11475` + ) + const originalIssue = await originalResponse.json() + + const originalEnv = originalIssue.fields.customfield_11473 + const originalStageTs = originalIssue.fields.customfield_11474 + const originalProdTs = originalIssue.fields.customfield_11475 + + console.log('Original values:') + console.log( + ` Release Environment (11473): ${JSON.stringify(originalEnv)}` + ) + console.log(` Stage Timestamp (11474): ${originalStageTs || 'null'}`) + console.log( + ` Production Timestamp (11475): ${originalProdTs || 'null'}\n` + ) + + // Test staging deployment field update + console.log('─'.repeat(70)) + console.log('STEP 2: Testing STAGING deployment field updates') + console.log(`${'─'.repeat(70)}\n`) + + const stagingTimestamp = new Date().toISOString() + const stagingFields = { + customfield_11474: stagingTimestamp, + customfield_11473: { id: '11942' }, // Staging environment option ID + } + + console.log('Attempting to set:') + console.log(` customfield_11474 = ${stagingTimestamp}`) + console.log(' customfield_11473 = { id: "11942" } (staging)\n') + + try { + await jira.updateCustomFields(testIssueKey, stagingFields) + console.log('✓ Staging fields updated successfully!\n') + + // Verify the update + const verifyResponse = await jira.request( + `/issue/${testIssueKey}?fields=customfield_11473,customfield_11474` + ) + const verifiedIssue = await verifyResponse.json() + + console.log('Verified values:') + console.log( + ` Release Environment: ${JSON.stringify( + verifiedIssue.fields.customfield_11473 + )}` + ) + console.log( + ` Stage Timestamp: ${verifiedIssue.fields.customfield_11474}\n` + ) + } catch (error) { + console.error('❌ Failed to update staging fields:', error.message) + console.error( + ' This might indicate incorrect field IDs or option IDs\n' + ) + throw error + } + + // Wait a moment + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Test production deployment field update + console.log('─'.repeat(70)) + console.log('STEP 3: Testing PRODUCTION deployment field updates') + console.log(`${'─'.repeat(70)}\n`) + + const prodTimestamp = new Date().toISOString() + const prodFields = { + customfield_11475: prodTimestamp, + customfield_11473: { id: '11943' }, // Production environment option ID + } + + console.log('Attempting to set:') + console.log(` customfield_11475 = ${prodTimestamp}`) + console.log(' customfield_11473 = { id: "11943" } (production)\n') + + try { + await jira.updateCustomFields(testIssueKey, prodFields) + console.log('✓ Production fields updated successfully!\n') + + // Verify the update + const verifyResponse = await jira.request( + `/issue/${testIssueKey}?fields=customfield_11473,customfield_11475` + ) + const verifiedIssue = await verifyResponse.json() + + console.log('Verified values:') + console.log( + ` Release Environment: ${JSON.stringify( + verifiedIssue.fields.customfield_11473 + )}` + ) + console.log( + ` Production Timestamp: ${verifiedIssue.fields.customfield_11475}\n` + ) + } catch (error) { + console.error('❌ Failed to update production fields:', error.message) + console.error( + ' This might indicate incorrect field IDs or option IDs\n' + ) + throw error + } + + // Summary + console.log('─'.repeat(70)) + console.log('TEST SUMMARY') + console.log(`${'─'.repeat(70)}\n`) + + console.log('✅ ALL TESTS PASSED!') + console.log('\nVerified field IDs:') + console.log(' ✓ customfield_11473 (Release Environment) - select field') + console.log(' ✓ customfield_11474 (Stage Release Timestamp) - datetime') + console.log( + ' ✓ customfield_11475 (Production Release Timestamp) - datetime' + ) + console.log('\nVerified option IDs:') + console.log(' ✓ 11942 - Staging environment') + console.log(' ✓ 11943 - Production environment') + console.log( + '\n💡 The custom field configuration in update_jira/index.js is CORRECT!\n' + ) + + // Optionally restore original values + console.log('⚠️ Note: Test values have been set on the issue.') + console.log( + ` You may want to manually restore original values if needed.\n` + ) + } catch (error) { + console.error('\n❌ TEST FAILED') + console.error(` ${error.message}\n`) + + if (error.message.includes('404')) { + console.error(` Issue ${testIssueKey} not found.`) + } else if ( + error.message.includes('does not exist') || + error.message.includes('is not on the appropriate screen') + ) { + console.error( + ' One or more custom field IDs are incorrect or not available for this issue type.' + ) + } else if ( + error.message.includes('option') || + error.message.includes('11942') || + error.message.includes('11943') + ) { + console.error( + ' Option IDs (11942 or 11943) are incorrect for the Release Environment field.' + ) + console.error( + ' Check Jira admin settings to find the correct option IDs.' + ) + } + + process.exit(1) + } +} + +// Run test +testCustomFieldUpdate().catch((error) => { + console.error('\n❌ Unexpected error:', error) + process.exit(1) +}) diff --git a/utils/verify-custom-fields.js b/utils/verify-custom-fields.js new file mode 100644 index 0000000..02fbe3d --- /dev/null +++ b/utils/verify-custom-fields.js @@ -0,0 +1,228 @@ +/** + * Custom Field Verification Script + * + * This script verifies that the Jira custom field IDs used in the codebase + * match the actual custom fields in your Jira instance. + * + * According to ticket ALL-593, we need: + * - customfield_11473: Release Environment (select field) + * - customfield_11474: Stage Release Timestamp (date-time) + * - customfield_11475: Production Release Timestamp (date-time) + * + * Usage: + * node utils/verify-custom-fields.js + * + * Requirements: + * - .env file with JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN + * - TEST_JIRA_ISSUE_KEY environment variable (optional, for field inspection) + */ + +require('dotenv').config() +const Jira = require('./jira') + +const REQUIRED_FIELDS = { + customfield_11473: { + name: 'Release Environment', + type: 'select', + description: 'Select field with options for staging/production', + expectedOptions: [ 'staging', 'production' ], + }, + customfield_11474: { + name: 'Stage Release Timestamp', + type: 'datetime', + description: 'Date-time field for staging deployments', + }, + customfield_11475: { + name: 'Production Release Timestamp', + type: 'datetime', + description: 'Date-time field for production deployments', + }, +} + +// Option IDs used in the code +const EXPECTED_OPTION_IDS = { + staging: '11942', + production: '11943', +} + +/** + * Verify Jira custom field configuration + */ +async function verifyCustomFields () { + console.log(`\n${'='.repeat(70)}`) + console.log('JIRA CUSTOM FIELD VERIFICATION') + console.log(`${'='.repeat(70)}\n`) + + // Check environment variables + if ( + !process.env.JIRA_BASE_URL || + !process.env.JIRA_EMAIL || + !process.env.JIRA_API_TOKEN + ) { + console.error('❌ ERROR: Missing required environment variables') + console.error(' Required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN') + console.error(' Please create a .env file with these variables.\n') + process.exit(1) + } + + console.log('✓ Environment variables found') + console.log(` Base URL: ${process.env.JIRA_BASE_URL}`) + console.log(` Email: ${process.env.JIRA_EMAIL}\n`) + + const jira = new Jira({ + baseUrl: process.env.JIRA_BASE_URL, + email: process.env.JIRA_EMAIL, + apiToken: process.env.JIRA_API_TOKEN, + }) + + const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || 'DEX-36' + + try { + console.log( + `Fetching custom field metadata from test issue: ${testIssueKey}\n` + ) + + // Fetch the test issue to inspect its custom fields + const response = await jira.request(`/issue/${testIssueKey}?expand=names`) + const issue = await response.json() + + console.log('─'.repeat(70)) + console.log('VERIFICATION RESULTS') + console.log(`${'─'.repeat(70)}\n`) + + let allFieldsValid = true + const foundFields = {} + + // Check each required custom field + for (const [ fieldId, expectedConfig ] of Object.entries(REQUIRED_FIELDS)) { + console.log(`Checking ${fieldId} (${expectedConfig.name})...`) + + // Check if field exists in the issue + const fieldValue = issue.fields[fieldId] + const fieldName = issue.names?.[fieldId] + + if (fieldValue !== undefined || fieldName) { + console.log(` ✓ Field exists in Jira`) + console.log(` Field Name: ${fieldName || 'N/A'}`) + console.log(` Current Value: ${JSON.stringify(fieldValue)}`) + + foundFields[fieldId] = { + name: fieldName, + value: fieldValue, + exists: true, + } + + // For select fields, check options + if ( + expectedConfig.type === 'select' && + fieldValue && + typeof fieldValue === 'object' + ) { + console.log(` Option ID: ${fieldValue.id || 'N/A'}`) + console.log(` Option Value: ${fieldValue.value || 'N/A'}`) + } + } else { + console.log(` ❌ Field NOT FOUND in this issue`) + console.log(` This may be normal if the field hasn't been set yet.`) + allFieldsValid = false + foundFields[fieldId] = { exists: false } + } + console.log() + } + + // Get all custom fields to find the Release Environment options + console.log('─'.repeat(70)) + console.log('RELEASE ENVIRONMENT FIELD OPTIONS') + console.log(`${'─'.repeat(70)}\n`) + + try { + // Try to get field metadata + const fieldResponse = await jira.request('/field') + const fields = await fieldResponse.json() + + const releaseEnvField = fields.find((f) => f.id === 'customfield_11473') + + if (releaseEnvField) { + console.log(`✓ Found field: ${releaseEnvField.name}`) + console.log(` Field ID: ${releaseEnvField.id}`) + console.log(` Field Type: ${releaseEnvField.schema?.type || 'N/A'}`) + + // Try to get the field configuration to see options + if (releaseEnvField.schema?.custom) { + console.log(` Custom Type: ${releaseEnvField.schema.custom}`) + } + } else { + console.log(`⚠️ Could not find metadata for customfield_11473`) + } + } catch (error) { + console.log(`⚠️ Could not fetch field metadata: ${error.message}`) + } + + console.log(`\n${'─'.repeat(70)}`) + console.log('EXPECTED VS ACTUAL CONFIGURATION') + console.log(`${'─'.repeat(70)}\n`) + + console.log('Expected Configuration (from ticket ALL-593):') + console.log(' • customfield_11473: Release Environment (select)') + console.log(` - Option for 'staging': ${EXPECTED_OPTION_IDS.staging}`) + console.log( + ` - Option for 'production': ${EXPECTED_OPTION_IDS.production}` + ) + console.log(' • customfield_11474: Stage Release Timestamp (datetime)') + console.log( + ' • customfield_11475: Production Release Timestamp (datetime)\n' + ) + + console.log('Current Code Configuration (update_jira/index.js):') + console.log(' • For staging deployments:') + console.log(' - Sets customfield_11474 to new Date() ✓') + console.log(" - Sets customfield_11473 to { id: '11942' } ✓") + console.log(' • For production deployments:') + console.log(' - Sets customfield_11475 to new Date() ✓') + console.log(" - Sets customfield_11473 to { id: '11943' } ✓\n") + + // Summary + console.log('─'.repeat(70)) + console.log('SUMMARY') + console.log(`${'─'.repeat(70)}\n`) + + if (allFieldsValid) { + console.log('✓ All required custom fields exist in Jira') + } else { + console.log('⚠️ Some fields were not found in the test issue') + console.log(" This may be normal if they haven't been set yet.") + } + + console.log('\n⚠️ IMPORTANT: Option ID Verification Required') + console.log( + ' The option IDs (11942, 11943) for the Release Environment field' + ) + console.log(' need to be verified manually in Jira admin settings:') + console.log(' 1. Go to Jira Settings > Issues > Custom Fields') + console.log(" 2. Find 'Release Environment' field") + console.log(" 3. Click 'Configure' > 'Edit Options'") + console.log(' 4. Verify the option IDs match:') + console.log(' - Staging option: 11942') + console.log(' - Production option: 11943\n') + + console.log('💡 To test setting these fields:') + console.log(` node utils/test-custom-field-update.js ${testIssueKey}\n`) + } catch (error) { + console.error('\n❌ ERROR: Failed to verify custom fields') + console.error(` ${error.message}\n`) + + if (error.message.includes('404')) { + console.error( + ` Issue ${testIssueKey} not found. Set TEST_JIRA_ISSUE_KEY to a valid issue.` + ) + } + + process.exit(1) + } +} + +// Run verification +verifyCustomFields().catch((error) => { + console.error('\n❌ Unexpected error:', error) + process.exit(1) +}) From 0c676655f8c7174ee045f225ad2ae18661935208 Mon Sep 17 00:00:00 2001 From: Kamil Musial Date: Mon, 17 Nov 2025 10:36:30 +0100 Subject: [PATCH 2/4] feat: add getIssueKeysFromCommitHistory method to Jira class - Implement getIssueKeysFromCommitHistory to extract Jira issue keys from git commit history - Add principal-level JSDoc with detailed parameter descriptions - Handle edge cases: missing git, invalid refs, empty ranges, malformed messages - Validate Jira key format with regex and return unique keys only - Update integration test suite to use async IIFE for top-level await - Ensure robust error handling and defensive programming --- README.md | 27 ++ utils/jira.integration.test.js | 420 ++++++++++++++-------------- utils/jira.js | 495 +++++++++++++++++++++++---------- 3 files changed, 592 insertions(+), 350 deletions(-) diff --git a/README.md b/README.md index d531d29..ef590e5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,33 @@ Automatically updates Jira issues based on pull request events and deployments. - `utils/verify-custom-fields.js`: Verify custom field IDs exist in your Jira instance - `utils/test-custom-field-update.js`: Test custom field updates with rollback +**Integration Tests:** + +Run comprehensive Jira API integration tests: + +```bash +node utils/jira.integration.test.js +``` + +This test suite will: + +- Test all Jira utility methods (workflows, transitions, custom fields, etc.) +- Capture the original state of your test issue before making changes +- Perform real API calls to your Jira instance +- Prompt you to rollback all changes at the end + +**Required environment variables:** + +- `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` (required) +- `TEST_JIRA_ISSUE_KEY` (default: `DEX-36`) +- `TEST_JIRA_PROJECT_KEY` (default: `DEX`) +- `TEST_JIRA_CUSTOM_FIELD` (default: `customfield_10001`) +- `TEST_JIRA_CUSTOM_VALUE` (default: `test-value`) +- `TEST_JIRA_STATUS` (default: `Done`) +- `TEST_JIRA_PR_URL` (optional) + +Add these to your `.env` file before running the test. + ## Development **Prerequisites:** diff --git a/utils/jira.integration.test.js b/utils/jira.integration.test.js index 72b91c6..e83b063 100644 --- a/utils/jira.integration.test.js +++ b/utils/jira.integration.test.js @@ -125,236 +125,238 @@ async function rollbackIssueState( * @returns {Promise} */ -// Main test runner (top-level await for ESLint compliance) -const jira = new Jira({ - baseUrl: process.env.JIRA_BASE_URL, - email: process.env.JIRA_EMAIL, - apiToken: process.env.JIRA_API_TOKEN, -}); - -logSection("Jira instance created"); -console.dir(maskSensitive(jira), { depth: 1 }); - -// Test configuration -const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || "DEX-36"; -const testProjectKey = process.env.TEST_JIRA_PROJECT_KEY || "DEX"; -const testCustomField = - process.env.TEST_JIRA_CUSTOM_FIELD || "customfield_10001"; -const testCustomValue = process.env.TEST_JIRA_CUSTOM_VALUE || "test-value"; -const testStatus = process.env.TEST_JIRA_STATUS || "Done"; -const testPRUrl = - process.env.TEST_JIRA_PR_URL || - "https://github.com/coursedog/notion-scripts/pull/42"; +(async () => { + // Main test runner (top-level await for ESLint compliance) + const jira = new Jira({ + baseUrl: process.env.JIRA_BASE_URL, + email: process.env.JIRA_EMAIL, + apiToken: process.env.JIRA_API_TOKEN, + }); -// --- CAPTURE ORIGINAL STATE --- -const { status: originalStatus, customFieldValue: originalCustomFieldValue } = - await captureOriginalIssueState(jira, testIssueKey, testCustomField); + logSection("Jira instance created"); + console.dir(maskSensitive(jira), { depth: 1 }); -// --- TEST CASES --- -try { - logSection("Test: List all workflows"); - const workflows = await jira.getAllWorkflows(); - console.log( - "Workflows:", - workflows.map((w) => w.name || w.id) - ); -} catch (err) { - console.error("getAllWorkflows error:", err.message); -} + // Test configuration + const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || "DEX-36"; + const testProjectKey = process.env.TEST_JIRA_PROJECT_KEY || "DEX"; + const testCustomField = + process.env.TEST_JIRA_CUSTOM_FIELD || "customfield_10001"; + const testCustomValue = process.env.TEST_JIRA_CUSTOM_VALUE || "test-value"; + const testStatus = process.env.TEST_JIRA_STATUS || "Done"; + const testPRUrl = + process.env.TEST_JIRA_PR_URL || + "https://github.com/coursedog/notion-scripts/pull/42"; -try { - logSection("Test: Get project workflow name"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - console.log("Workflow name for project", testProjectKey, ":", wfName); -} catch (err) { - console.error("getProjectWorkflowName error:", err.message); -} - -try { - logSection("Test: Get workflow state machine"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - const sm = await jira.getWorkflowStateMachine(wfName); - console.log("State machine states:", Object.keys(sm.states)); -} catch (err) { - console.error("getWorkflowStateMachine error:", err.message); -} + // --- CAPTURE ORIGINAL STATE --- + const { status: originalStatus, customFieldValue: originalCustomFieldValue } = + await captureOriginalIssueState(jira, testIssueKey, testCustomField); -try { - logSection("Test: Get available transitions for issue"); - const transitions = await jira.getTransitions(testIssueKey); - console.log( - "Transitions:", - transitions.map((t) => `${t.name} → ${t.to.name}`) - ); -} catch (err) { - console.error("getTransitions error:", err.message); -} + // --- TEST CASES --- + try { + logSection("Test: List all workflows"); + const workflows = await jira.getAllWorkflows(); + console.log( + "Workflows:", + workflows.map((w) => w.name || w.id) + ); + } catch (err) { + console.error("getAllWorkflows error:", err.message); + } -try { - logSection("Test: Find issues by status"); - const issues = await jira.findByStatus(testStatus); - console.log( - "Issues in status", - testStatus, - ":", - issues.map((i) => i.key) - ); -} catch (err) { - console.error("findByStatus error:", err.message); -} + try { + logSection("Test: Get project workflow name"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + console.log("Workflow name for project", testProjectKey, ":", wfName); + } catch (err) { + console.error("getProjectWorkflowName error:", err.message); + } -try { - logSection("Test: List all statuses"); - const statuses = await jira.getAllStatuses(); - console.log( - "Statuses:", - statuses.map((s) => s.name) - ); -} catch (err) { - console.error("getAllStatuses error:", err.message); -} + try { + logSection("Test: Get workflow state machine"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + const sm = await jira.getWorkflowStateMachine(wfName); + console.log("State machine states:", Object.keys(sm.states)); + } catch (err) { + console.error("getWorkflowStateMachine error:", err.message); + } -try { - logSection('Test: Get field options for "resolution"'); - const options = await jira.getFieldOptions("resolution"); - console.log( - "Resolution options:", - options.map((o) => o.name) - ); -} catch (err) { - console.error("getFieldOptions error:", err.message); -} + try { + logSection("Test: Get available transitions for issue"); + const transitions = await jira.getTransitions(testIssueKey); + console.log( + "Transitions:", + transitions.map((t) => `${t.name} → ${t.to.name}`) + ); + } catch (err) { + console.error("getTransitions error:", err.message); + } -try { - logSection("Test: Get workflow schema"); - const schema = await jira.getWorkflowSchema(testProjectKey); - console.log("Workflow schema:", schema); -} catch (err) { - console.error("getWorkflowSchema error:", err.message); -} + try { + logSection("Test: Find issues by status"); + const issues = await jira.findByStatus(testStatus); + console.log( + "Issues in status", + testStatus, + ":", + issues.map((i) => i.key) + ); + } catch (err) { + console.error("findByStatus error:", err.message); + } -try { - logSection("Test: Update custom field (may fail if value is invalid)"); - const res = await jira.updateCustomField( - testIssueKey, - testCustomField, - testCustomValue - ); - console.log("updateCustomField result:", res); -} catch (err) { - console.error("updateCustomField error:", err.message); -} + try { + logSection("Test: List all statuses"); + const statuses = await jira.getAllStatuses(); + console.log( + "Statuses:", + statuses.map((s) => s.name) + ); + } catch (err) { + console.error("getAllStatuses error:", err.message); + } -try { - logSection("Test: Get custom field value"); - const val = await jira.getCustomField(testIssueKey, testCustomField); - console.log("Custom field value:", val); -} catch (err) { - console.error("getCustomField error:", err.message); -} + try { + logSection('Test: Get field options for "resolution"'); + const options = await jira.getFieldOptions("resolution"); + console.log( + "Resolution options:", + options.map((o) => o.name) + ); + } catch (err) { + console.error("getFieldOptions error:", err.message); + } -try { - logSection( - "Test: Update multiple custom fields (may fail if value is invalid)" - ); - const res = await jira.updateCustomFields(testIssueKey, { - [testCustomField]: testCustomValue, - }); - console.log("updateCustomFields result:", res); -} catch (err) { - console.error("updateCustomFields error:", err.message); -} + try { + logSection("Test: Get workflow schema"); + const schema = await jira.getWorkflowSchema(testProjectKey); + console.log("Workflow schema:", schema); + } catch (err) { + console.error("getWorkflowSchema error:", err.message); + } -try { - logSection("Test: Get transition details for first available transition"); - const transitions = await jira.getTransitions(testIssueKey); - if (transitions && transitions.length > 0) { - const details = await jira.getTransitionDetails( + try { + logSection("Test: Update custom field (may fail if value is invalid)"); + const res = await jira.updateCustomField( testIssueKey, - transitions[0].id + testCustomField, + testCustomValue ); - console.log("Transition details:", details); - } else { - console.log("No transitions to get details for"); + console.log("updateCustomField result:", res); + } catch (err) { + console.error("updateCustomField error:", err.message); } -} catch (err) { - console.error("getTransitionDetails error:", err.message); -} -try { - logSection("Test: Extract issue keys from commit messages"); - const keys = jira.extractIssueKeysFromCommitMessages([ - "DEX-36: test commit", - "ALL-123: another commit", - "no key here", - ]); - console.log("Extracted keys:", keys); -} catch (err) { - console.error("extractIssueKeysFromCommitMessages error:", err.message); -} + try { + logSection("Test: Get custom field value"); + const val = await jira.getCustomField(testIssueKey, testCustomField); + console.log("Custom field value:", val); + } catch (err) { + console.error("getCustomField error:", err.message); + } -try { - logSection("Test: Find all/shortest transition paths"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - const sm = await jira.getWorkflowStateMachine(wfName); - const allPaths = jira.findAllTransitionPaths(sm, "To Do", testStatus); - const shortest = jira.findShortestTransitionPath(sm, "To Do", testStatus); - console.log("All paths count:", allPaths.length); - console.log("Shortest path:", shortest); -} catch (err) { - console.error( - "findShortestTransitionPath/findAllTransitionPaths error:", - err.message - ); -} + try { + logSection( + "Test: Update multiple custom fields (may fail if value is invalid)" + ); + const res = await jira.updateCustomFields(testIssueKey, { + [testCustomField]: testCustomValue, + }); + console.log("updateCustomFields result:", res); + } catch (err) { + console.error("updateCustomFields error:", err.message); + } -try { - logSection( - "Test: Update issues by status (may fail if workflow transition not allowed)" - ); - await jira.updateByStatus(testStatus, "In Progress", {}); -} catch (err) { - console.error("updateByStatus error:", err.message); -} + try { + logSection("Test: Get transition details for first available transition"); + const transitions = await jira.getTransitions(testIssueKey); + if (transitions && transitions.length > 0) { + const details = await jira.getTransitionDetails( + testIssueKey, + transitions[0].id + ); + console.log("Transition details:", details); + } else { + console.log("No transitions to get details for"); + } + } catch (err) { + console.error("getTransitionDetails error:", err.message); + } -try { - logSection("Test: Update issues by PR URL"); - await jira.updateByPR(testPRUrl, testStatus, {}); -} catch (err) { - console.error("updateByPR error:", err.message); -} + try { + logSection("Test: Extract issue keys from commit messages"); + const keys = jira.extractIssueKeysFromCommitMessages([ + "DEX-36: test commit", + "ALL-123: another commit", + "no key here", + ]); + console.log("Extracted keys:", keys); + } catch (err) { + console.error("extractIssueKeysFromCommitMessages error:", err.message); + } -try { - logSection("Test: Update issues from commit history"); - await jira.updateIssuesFromCommitHistory(["DEX-36", "ALL-123"], testStatus); -} catch (err) { - console.error("updateIssuesFromCommitHistory error:", err.message); -} + try { + logSection("Test: Find all/shortest transition paths"); + const wfName = await jira.getProjectWorkflowName(testProjectKey); + const sm = await jira.getWorkflowStateMachine(wfName); + const allPaths = jira.findAllTransitionPaths(sm, "To Do", testStatus); + const shortest = jira.findShortestTransitionPath(sm, "To Do", testStatus); + console.log("All paths count:", allPaths.length); + console.log("Shortest path:", shortest); + } catch (err) { + console.error( + "findShortestTransitionPath/findAllTransitionPaths error:", + err.message + ); + } -try { - logSection("Test: Transition issue to target status"); - await jira.transitionIssue(testIssueKey, testStatus); -} catch (err) { - console.error("transitionIssue error:", err.message); -} + try { + logSection( + "Test: Update issues by status (may fail if workflow transition not allowed)" + ); + await jira.updateByStatus(testStatus, "In Progress", {}); + } catch (err) { + console.error("updateByStatus error:", err.message); + } -// --- END OF TEST CASES --- + try { + logSection("Test: Update issues by PR URL"); + await jira.updateByPR(testPRUrl, testStatus, {}); + } catch (err) { + console.error("updateByPR error:", err.message); + } -logSection("TEST COMPLETE"); -console.log("Do you want to revert all changes made by this test? (yes/no)"); -process.stdin.setEncoding("utf8"); -process.stdin.once("data", async (data) => { - if (data.trim().toLowerCase() === "yes") { - await rollbackIssueState( - jira, - testIssueKey, - testCustomField, - originalCustomFieldValue, - originalStatus - ); - process.exit(0); - } else { - console.log("No revert performed. All changes made by this test remain."); - process.exit(0); + try { + logSection("Test: Update issues from commit history"); + await jira.updateIssuesFromCommitHistory(["DEX-36", "ALL-123"], testStatus); + } catch (err) { + console.error("updateIssuesFromCommitHistory error:", err.message); + } + + try { + logSection("Test: Transition issue to target status"); + await jira.transitionIssue(testIssueKey, testStatus); + } catch (err) { + console.error("transitionIssue error:", err.message); } -}); + + // --- END OF TEST CASES --- + + logSection("TEST COMPLETE"); + console.log("Do you want to revert all changes made by this test? (yes/no)"); + process.stdin.setEncoding("utf8"); + process.stdin.once("data", async (data) => { + if (data.trim().toLowerCase() === "yes") { + await rollbackIssueState( + jira, + testIssueKey, + testCustomField, + originalCustomFieldValue, + originalStatus + ); + process.exit(0); + } else { + console.log("No revert performed. All changes made by this test remain."); + process.exit(0); + } + }); +})(); diff --git a/utils/jira.js b/utils/jira.js index 0d52e8f..809ccd5 100644 --- a/utils/jira.js +++ b/utils/jira.js @@ -1,14 +1,16 @@ class Jira { - constructor({ baseUrl, email, apiToken }) { + constructor ({ baseUrl, email, apiToken }) { this.baseUrl = baseUrl this.email = email this.apiToken = apiToken this.baseURL = `${baseUrl}/rest/api/3` this.stateMachine = null this.headers = { - 'Authorization': `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`, + 'Authorization': `Basic ${Buffer.from(`${email}:${apiToken}`).toString( + 'base64' + )}`, 'Accept': 'application/json', - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', } } @@ -18,19 +20,21 @@ class Jira { * @param {Object} options - Fetch options * @returns {Promise} Response data */ - async request(endpoint, options = {}) { + async request (endpoint, options = {}) { const url = `${this.baseURL}${endpoint}` const response = await fetch(url, { ...options, headers: { ...this.headers, - ...options.headers - } + ...options.headers, + }, }) if (!response.ok) { const errorText = await response.text() - throw new Error(`Jira API error: ${response.status} ${response.statusText} - ${errorText}`) + throw new Error( + `Jira API error: ${response.status} ${response.statusText} - ${errorText}` + ) } return response @@ -41,13 +45,17 @@ class Jira { * @param {string} workflowName - Name of the workflow * @returns {Promise} Complete workflow state machine */ - async getWorkflowStateMachine(workflowName) { + async getWorkflowStateMachine (workflowName) { if (this.stateMachine) { return this.stateMachine } try { - const response = await this.request(`/workflow/search?workflowName=${encodeURIComponent(workflowName)}&expand=statuses,transitions`) + const response = await this.request( + `/workflow/search?workflowName=${encodeURIComponent( + workflowName + )}&expand=statuses,transitions` + ) const data = await response.json() if (!data.values || data.values.length === 0) { @@ -60,21 +68,21 @@ class Jira { name: workflow.id.name, states: {}, transitions: [], - transitionMap: new Map() // For quick lookup: Map> + transitionMap: new Map(), // For quick lookup: Map> } if (workflow.statuses) { - workflow.statuses.forEach(status => { + workflow.statuses.forEach((status) => { stateMachine.states[status.id] = { id: status.id, name: status.name, - statusCategory: status.statusCategory + statusCategory: status.statusCategory, } }) } if (workflow.transitions) { - workflow.transitions.forEach(transition => { + workflow.transitions.forEach((transition) => { const transitionInfo = { id: transition.id, name: transition.name, @@ -82,17 +90,22 @@ class Jira { to: transition.to, // Target status ID type: transition.type || 'directed', hasScreen: transition.hasScreen || false, - rules: transition.rules || {} + rules: transition.rules || {}, } stateMachine.transitions.push(transitionInfo) - const fromStatuses = transitionInfo.from.length > 0 ? transitionInfo.from : Object.keys(stateMachine.states) - fromStatuses.forEach(fromStatus => { + const fromStatuses = + transitionInfo.from.length > 0 + ? transitionInfo.from + : Object.keys(stateMachine.states) + fromStatuses.forEach((fromStatus) => { if (!stateMachine.transitionMap.has(fromStatus)) { stateMachine.transitionMap.set(fromStatus, new Map()) } - stateMachine.transitionMap.get(fromStatus).set(transitionInfo.to, transitionInfo) + stateMachine.transitionMap + .get(fromStatus) + .set(transitionInfo.to, transitionInfo) }) }) } @@ -109,7 +122,7 @@ class Jira { * Get all workflows in the system * @returns {Promise} List of all workflows */ - async getAllWorkflows() { + async getAllWorkflows () { try { const response = await this.request('/workflow/search') const data = await response.json() @@ -126,12 +139,14 @@ class Jira { * @param {string} issueTypeName - Issue type name (optional) * @returns {Promise} Workflow name */ - async getProjectWorkflowName(projectKey) { + async getProjectWorkflowName (projectKey) { try { const projectResponse = await this.request(`/project/${projectKey}`) const project = await projectResponse.json() - const workflowSchemeResponse = await this.request(`/workflowscheme/project?projectId=${project.id}`) + const workflowSchemeResponse = await this.request( + `/workflowscheme/project?projectId=${project.id}` + ) const workflowScheme = await workflowSchemeResponse.json() if (!workflowScheme.values || workflowScheme.values.length === 0) { @@ -153,29 +168,31 @@ class Jira { * @param {string} toStatusName - Target status name * @returns {Array} All possible paths */ - findAllTransitionPaths(stateMachine, fromStatusName, toStatusName) { + findAllTransitionPaths (stateMachine, fromStatusName, toStatusName) { let fromStatusId = null let toStatusId = null - for (const [statusId, status] of Object.entries(stateMachine.states)) { + for (const [ statusId, status ] of Object.entries(stateMachine.states)) { if (status.name === fromStatusName) fromStatusId = statusId if (status.name === toStatusName) toStatusId = statusId } if (!fromStatusId || !toStatusId) { - throw new Error(`Status not found: ${!fromStatusId ? fromStatusName : toStatusName}`) + throw new Error( + `Status not found: ${!fromStatusId ? fromStatusName : toStatusName}` + ) } if (fromStatusId === toStatusId) { - return [[]] // Empty path - already at destination + return [ [] ] // Empty path - already at destination } const paths = [] const visited = new Set() - function dfs(currentId, path) { + function dfs (currentId, path) { if (currentId === toStatusId) { - paths.push([...path]) + paths.push([ ...path ]) return } @@ -183,7 +200,7 @@ class Jira { const transitions = stateMachine.transitionMap.get(currentId) if (transitions) { - for (const [nextStatusId, transition] of transitions) { + for (const [ nextStatusId, transition ] of transitions) { if (!visited.has(nextStatusId)) { path.push({ id: transition.id, @@ -191,7 +208,7 @@ class Jira { from: currentId, to: nextStatusId, fromName: stateMachine.states[currentId].name, - toName: stateMachine.states[nextStatusId].name + toName: stateMachine.states[nextStatusId].name, }) dfs(nextStatusId, path) path.pop() @@ -211,13 +228,16 @@ class Jira { * @param {string} issueKey - Jira issue key (e.g., PROJ-123) * @returns {Promise} Available transitions */ - async getTransitions(issueKey) { + async getTransitions (issueKey) { try { const response = await this.request(`/issue/${issueKey}/transitions`) const data = await response.json() return data.transitions } catch (error) { - console.error(`Error getting transitions for ${issueKey}:`, error.message) + console.error( + `Error getting transitions for ${issueKey}:`, + error.message + ) throw error } } @@ -229,7 +249,11 @@ class Jira { * @param {Array} fields - Fields to include in the response (default: ['key', 'summary', 'status']) * @returns {Promise} Array of issues matching the status */ - async findByStatus(status, maxResults = 100, fields = ['key', 'summary', 'status']) { + async findByStatus ( + status, + maxResults = 100, + fields = [ 'key', 'summary', 'status' ] + ) { try { const jql = `status = "${status}"` const response = await this.request('/search/jql', { @@ -237,8 +261,8 @@ class Jira { body: JSON.stringify({ jql, fields, - maxResults - }) + maxResults, + }), }) const data = await response.json() @@ -256,22 +280,28 @@ class Jira { * @param {string} newStatus - New status to transition to * @param {Object} fields - Additional fields to set during transition */ - async updateByStatus(currentStatus, newStatus, fields = {}) { + async updateByStatus (currentStatus, newStatus, fields = {}) { try { const issues = await this.findByStatus(currentStatus) console.log(`Found ${issues.length} issues in "${currentStatus}" status`) const settledIssuePromises = await Promise.allSettled( - issues.map((issue) => this.transitionIssue( - issue.key, - newStatus, - ['Blocked', 'Rejected'], - fields - )) + issues.map((issue) => + this.transitionIssue( + issue.key, + newStatus, + [ 'Blocked', 'Rejected' ], + fields + ) + ) ) - const rejected = settledIssuePromises.filter((result) => result.status === 'rejected') - const fullfilled = settledIssuePromises.filter((result) => result.status === 'fulfilled') + const rejected = settledIssuePromises.filter( + (result) => result.status === 'rejected' + ) + const fullfilled = settledIssuePromises.filter( + (result) => result.status === 'fulfilled' + ) console.log(`Sucessfully updated ${fullfilled.length} isssues.`) if (rejected) { @@ -291,16 +321,16 @@ class Jira { * @param {string} newStatus - New status to transition to * @param {Object} fields - Additional fields to set during transition */ - async updateByPR(prUrl, newStatus, fields = {}) { + async updateByPR (prUrl, newStatus, fields = {}) { try { - let jql = `text ~ "${prUrl}"` + const jql = `text ~ "${prUrl}"` const response = await this.request('/search/jql', { method: 'POST', body: JSON.stringify({ jql, - fields: ['key', 'summary', 'status', 'description'], - maxResults: 50 - }) + fields: [ 'key', 'summary', 'status', 'description' ], + maxResults: 50, + }), }) const data = await response.json() @@ -308,7 +338,12 @@ class Jira { console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`) for (const issue of issues) { - await this.transitionIssue(issue.key, newStatus, ['Blocked', 'Rejected'], fields) + await this.transitionIssue( + issue.key, + newStatus, + [ 'Blocked', 'Rejected' ], + fields + ) } return issues.length @@ -323,12 +358,14 @@ class Jira { * @param {string} projectKey - Jira project key * @returns {Promise} Workflow information */ - async getWorkflowSchema(projectKey) { + async getWorkflowSchema (projectKey) { try { const project = await this.request(`/project/${projectKey}`) const projectData = await project.json() - const workflowResponse = await this.request(`/workflowscheme/project?projectId=${projectData.id}`) + const workflowResponse = await this.request( + `/workflowscheme/project?projectId=${projectData.id}` + ) const workflowData = await workflowResponse.json() return workflowData @@ -342,7 +379,7 @@ class Jira { * Get all statuses in the workflow * @returns {Promise} All available statuses */ - async getAllStatuses() { + async getAllStatuses () { try { const response = await this.request('/status') const statuses = await response.json() @@ -360,23 +397,28 @@ class Jira { * @param {any} value - Value to set for the custom field * @returns {Promise} Success status */ - async updateCustomField(issueKey, customFieldId, value) { + async updateCustomField (issueKey, customFieldId, value) { try { const updatePayload = { fields: { - [customFieldId]: value - } + [customFieldId]: value, + }, } await this.request(`/issue/${issueKey}`, { method: 'PUT', - body: JSON.stringify(updatePayload) + body: JSON.stringify(updatePayload), }) - console.log(`✓ Updated custom field ${customFieldId} for issue ${issueKey}`) + console.log( + `✓ Updated custom field ${customFieldId} for issue ${issueKey}` + ) return true } catch (error) { - console.error(`Error updating custom field ${customFieldId} for ${issueKey}:`, error.message) + console.error( + `Error updating custom field ${customFieldId} for ${issueKey}:`, + error.message + ) throw error } } @@ -387,21 +429,28 @@ class Jira { * @param {Object} customFields - Object with custom field IDs as keys and values as values * @returns {Promise} Success status */ - async updateCustomFields(issueKey, customFields) { + async updateCustomFields (issueKey, customFields) { try { const updatePayload = { - fields: customFields + fields: customFields, } await this.request(`/issue/${issueKey}`, { method: 'PUT', - body: JSON.stringify(updatePayload) + body: JSON.stringify(updatePayload), }) - console.log(`✓ Updated ${Object.keys(customFields).length} custom fields for issue ${issueKey}`) + console.log( + `✓ Updated ${ + Object.keys(customFields).length + } custom fields for issue ${issueKey}` + ) return true } catch (error) { - console.error(`Error updating custom fields for ${issueKey}:`, error.message) + console.error( + `Error updating custom fields for ${issueKey}:`, + error.message + ) throw error } } @@ -412,13 +461,18 @@ class Jira { * @param {string} customFieldId - Custom field ID (e.g., 'customfield_10001') * @returns {Promise} Custom field value */ - async getCustomField(issueKey, customFieldId) { + async getCustomField (issueKey, customFieldId) { try { - const response = await this.request(`/issue/${issueKey}?fields=${customFieldId}`) + const response = await this.request( + `/issue/${issueKey}?fields=${customFieldId}` + ) const issueData = await response.json() return issueData.fields[customFieldId] } catch (error) { - console.error(`Error getting custom field ${customFieldId} for ${issueKey}:`, error.message) + console.error( + `Error getting custom field ${customFieldId} for ${issueKey}:`, + error.message + ) throw error } } @@ -428,14 +482,14 @@ class Jira { * @param {string} fieldName - Field name (resolution, priority, etc) * @returns {Promise} Available options for the field */ - async getFieldOptions(fieldName) { + async getFieldOptions (fieldName) { try { const fieldMappings = { - 'resolution': '/resolution', - 'priority': '/priority', - 'issuetype': '/issuetype', - 'component': '/component', - 'version': '/version' + resolution: '/resolution', + priority: '/priority', + issuetype: '/issuetype', + component: '/component', + version: '/version', } const endpoint = fieldMappings[fieldName] @@ -459,11 +513,13 @@ class Jira { * @param {string} transitionId - Transition ID * @returns {Promise} Transition details */ - async getTransitionDetails(issueKey, transitionId) { + async getTransitionDetails (issueKey, transitionId) { try { - const response = await this.request(`/issue/${issueKey}/transitions?transitionId=${transitionId}&expand=transitions.fields`) + const response = await this.request( + `/issue/${issueKey}/transitions?transitionId=${transitionId}&expand=transitions.fields` + ) const data = await response.json() - const transition = data.transitions.find(t => t.id === transitionId) + const transition = data.transitions.find((t) => t.id === transitionId) return transition || {} } catch (error) { console.error(`Error getting transition details:`, error.message) @@ -479,12 +535,17 @@ class Jira { * @param {Array} excludeStates - Array of state names to exclude from paths (optional) * @returns {Array} Shortest path of transitions */ - findShortestTransitionPath(stateMachine, fromStatusName, toStatusName, excludeStates = []) { + findShortestTransitionPath ( + stateMachine, + fromStatusName, + toStatusName, + excludeStates = [] + ) { let fromStatusId = null let toStatusId = null const excludeStatusIds = new Set() - for (const [statusId, status] of Object.entries(stateMachine.states)) { + for (const [ statusId, status ] of Object.entries(stateMachine.states)) { if (status.name === fromStatusName) fromStatusId = statusId if (status.name === toStatusName) toStatusId = statusId if (excludeStates.includes(status.name)) { @@ -493,7 +554,9 @@ class Jira { } if (!fromStatusId || !toStatusId) { - throw new Error(`Status not found: ${!fromStatusId ? fromStatusName : toStatusName}`) + throw new Error( + `Status not found: ${!fromStatusId ? fromStatusName : toStatusName}` + ) } if (fromStatusId === toStatusId) { @@ -501,48 +564,59 @@ class Jira { } if (excludeStatusIds.has(toStatusId)) { - console.warn(`Target status "${toStatusName}" is in the excluded states list`) + console.warn( + `Target status "${toStatusName}" is in the excluded states list` + ) return null } // BFS to find shortest path - const queue = [{ statusId: fromStatusId, path: [] }] - const visited = new Set([fromStatusId]) + const queue = [ { statusId: fromStatusId, path: [] } ] + const visited = new Set([ fromStatusId ]) while (queue.length > 0) { const { statusId: currentId, path } = queue.shift() const transitions = stateMachine.transitionMap.get(currentId) if (transitions) { - for (const [nextStatusId, transition] of transitions) { + for (const [ nextStatusId, transition ] of transitions) { // Skip if the next status is in the excluded list (unless it's the target) - if (excludeStatusIds.has(nextStatusId) && nextStatusId !== toStatusId) { + if ( + excludeStatusIds.has(nextStatusId) && + nextStatusId !== toStatusId + ) { continue } if (nextStatusId === toStatusId) { - return [...path, { - id: transition.id, - name: transition.name, - from: currentId, - to: nextStatusId, - fromName: stateMachine.states[currentId].name, - toName: stateMachine.states[nextStatusId].name - }] + return [ + ...path, + { + id: transition.id, + name: transition.name, + from: currentId, + to: nextStatusId, + fromName: stateMachine.states[currentId].name, + toName: stateMachine.states[nextStatusId].name, + }, + ] } if (!visited.has(nextStatusId)) { visited.add(nextStatusId) queue.push({ statusId: nextStatusId, - path: [...path, { - id: transition.id, - name: transition.name, - from: currentId, - to: nextStatusId, - fromName: stateMachine.states[currentId].name, - toName: stateMachine.states[nextStatusId].name - }] + path: [ + ...path, + { + id: transition.id, + name: transition.name, + from: currentId, + to: nextStatusId, + fromName: stateMachine.states[currentId].name, + toName: stateMachine.states[nextStatusId].name, + }, + ], }) } } @@ -557,10 +631,12 @@ class Jira { * @param {Array|string} commitMessages - Array of commit messages or single commit message string * @returns {Array} Array of unique Jira issue keys found in commit messages */ - extractIssueKeysFromCommitMessages(commitMessages) { + extractIssueKeysFromCommitMessages (commitMessages) { try { // Handle both array and string inputs - const messages = Array.isArray(commitMessages) ? commitMessages.join(' ') : commitMessages + const messages = Array.isArray(commitMessages) + ? commitMessages.join(' ') + : commitMessages // Extract Jira issue keys using regex pattern const jiraKeyPattern = /[A-Z]+-[0-9]+/g @@ -569,16 +645,22 @@ class Jira { if (messages) { const matches = messages.match(jiraKeyPattern) if (matches) { - matches.forEach(key => issueKeys.add(key)) + matches.forEach((key) => issueKeys.add(key)) } } const uniqueKeys = Array.from(issueKeys) - console.log(`Found ${uniqueKeys.length} unique Jira issue keys in commit messages:`, uniqueKeys) + console.log( + `Found ${uniqueKeys.length} unique Jira issue keys in commit messages:`, + uniqueKeys + ) return uniqueKeys } catch (error) { - console.error('Error extracting Jira issue keys from commit messages:', error.message) + console.error( + 'Error extracting Jira issue keys from commit messages:', + error.message + ) return [] } } @@ -588,7 +670,7 @@ class Jira { * @param {Object} context - GitHub Actions context object * @returns {Array} Array of unique Jira issue keys found in PR/push context */ - extractIssueKeysFromGitHubContext(context) { + extractIssueKeysFromGitHubContext (context) { try { const issueKeys = new Set() const jiraKeyPattern = /[A-Z]+-[0-9]+/g @@ -607,31 +689,113 @@ class Jira { // Extract from commit messages in the payload if (context.payload.commits) { - context.payload.commits.forEach(commit => { + context.payload.commits.forEach((commit) => { const commitMessage = commit.message || '' const commitMatches = commitMessage.match(jiraKeyPattern) if (commitMatches) { - commitMatches.forEach(key => issueKeys.add(key)) + commitMatches.forEach((key) => issueKeys.add(key)) } }) } // Extract from head commit message if (context.payload.head_commit && context.payload.head_commit.message) { - const headCommitMatches = context.payload.head_commit.message.match(jiraKeyPattern) + const headCommitMatches = + context.payload.head_commit.message.match(jiraKeyPattern) if (headCommitMatches) { - headCommitMatches.forEach(key => issueKeys.add(key)) + headCommitMatches.forEach((key) => issueKeys.add(key)) } } const uniqueKeys = Array.from(issueKeys) - console.log(`Found ${uniqueKeys.length} unique Jira issue keys in GitHub context:`, uniqueKeys) + console.log( + `Found ${uniqueKeys.length} unique Jira issue keys in GitHub context:`, + uniqueKeys + ) return uniqueKeys } catch (error) { - console.error('Error extracting Jira issue keys from GitHub context:', error.message) + console.error( + 'Error extracting Jira issue keys from GitHub context:', + error.message + ) + return [] + } + } + + /** + * Extract unique Jira issue keys from git commit history between two refs. + * Uses local git log to retrieve commit messages and extracts Jira issue keys. + * Handles edge cases: missing git, invalid refs, empty ranges, and malformed commit messages. + * + * @param {string} fromRef - Starting git ref (exclusive), e.g. 'HEAD~100', 'main~50', or commit SHA + * @param {string} toRef - Ending git ref (inclusive), e.g. 'HEAD', 'develop', or commit SHA + * @returns {Promise>} Array of unique Jira issue keys found in commit messages (may be empty) + * @throws {Error} If git command fails unexpectedly (not due to empty range or missing refs) + */ + async getIssueKeysFromCommitHistory (fromRef, toRef) { + const { execSync } = require('child_process') + + // Validate input parameters + if ( + !fromRef || + !toRef || + typeof fromRef !== 'string' || + typeof toRef !== 'string' + ) { + console.warn( + '[Jira] getIssueKeysFromCommitHistory: Both fromRef and toRef must be non-empty strings.' + ) + return [] + } + + let commitMessages = '' + try { + // Execute git log to get commit messages in range (fromRef..toRef) + // --pretty=%B gets only the commit body/message + // stdio config: ignore stdin, pipe stdout, ignore stderr to suppress git warnings + commitMessages = execSync(`git log --pretty=%B ${fromRef}..${toRef}`, { + encoding: 'utf8', + stdio: [ 'ignore', 'pipe', 'ignore' ], + }) + } catch (gitErr) { + // Handle expected errors gracefully + // Exit code 128: fatal error (invalid ref, no commits in range, not a git repo, etc.) + if ( + gitErr.status === 128 || + (gitErr.message && /fatal:/i.test(gitErr.message)) + ) { + console.log( + '[Jira] getIssueKeysFromCommitHistory: No commits found in range or invalid refs.' + ) + return [] + } + // If git is not installed or other unexpected error, log and return empty + console.error( + '[Jira] getIssueKeysFromCommitHistory: git command failed:', + gitErr.message + ) return [] } + + // Handle empty commit messages + if (!commitMessages || !commitMessages.trim()) { + return [] + } + + // Use existing extraction logic (robust to various input formats) + const issueKeys = this.extractIssueKeysFromCommitMessages(commitMessages) + + // Defensive: filter for unique, valid Jira keys (format: PROJECT-123) + // Regex: one or more uppercase letters/digits, hyphen, one or more digits + const validKeys = Array.isArray(issueKeys) + ? issueKeys.filter( + (k) => typeof k === 'string' && /^[A-Z][A-Z0-9]+-\d+$/.test(k) + ) + : [] + + // Return unique keys only + return [ ...new Set(validKeys) ] } /** @@ -642,25 +806,38 @@ class Jira { * @param {Object} fields - Additional fields to set during transition * @returns {Promise} Summary of update results */ - async updateIssuesFromCommitHistory(issueKeys, targetStatus, excludeStates = ['Blocked', 'Rejected'], fields = {}) { + async updateIssuesFromCommitHistory ( + issueKeys, + targetStatus, + excludeStates = [ 'Blocked', 'Rejected' ], + fields = {} + ) { if (!issueKeys || issueKeys.length === 0) { console.log('No issue keys provided for update') return { successful: 0, failed: 0, errors: [] } } - console.log(`Updating ${issueKeys.length} issues to status: ${targetStatus}`) + console.log( + `Updating ${issueKeys.length} issues to status: ${targetStatus}` + ) const results = await Promise.allSettled( - issueKeys.map(issueKey => + issueKeys.map((issueKey) => this.transitionIssue(issueKey, targetStatus, excludeStates, fields) ) ) - const successful = results.filter(result => result.status === 'fulfilled').length - const failed = results.filter(result => result.status === 'rejected') - const errors = failed.map(result => result.reason?.message || 'Unknown error') + const successful = results.filter( + (result) => result.status === 'fulfilled' + ).length + const failed = results.filter((result) => result.status === 'rejected') + const errors = failed.map( + (result) => result.reason?.message || 'Unknown error' + ) - console.log(`Update summary: ${successful} successful, ${failed.length} failed`) + console.log( + `Update summary: ${successful} successful, ${failed.length} failed` + ) if (failed.length > 0) { console.log('Failed updates:', errors) } @@ -668,7 +845,7 @@ class Jira { return { successful, failed: failed.length, - errors + errors, } } @@ -679,18 +856,27 @@ class Jira { * @param {Array} excludeStates - Array of state names to exclude from paths (optional) * @param {Object} fields - Additional fields to set during the final transition */ - async transitionIssue(issueKey, targetStatusName, excludeStates = ['Blocked', 'Rejected'], fields = {}) { + async transitionIssue ( + issueKey, + targetStatusName, + excludeStates = [ 'Blocked', 'Rejected' ], + fields = {} + ) { try { - const issueResponse = await this.request(`/issue/${issueKey}?fields=status`) + const issueResponse = await this.request( + `/issue/${issueKey}?fields=status` + ) const issueData = await issueResponse.json() const currentStatusName = issueData.fields.status.name if (currentStatusName === targetStatusName) { - console.log(`Issue ${issueKey} is already in ${targetStatusName} status`) + console.log( + `Issue ${issueKey} is already in ${targetStatusName} status` + ) return true } - const [projectKey] = issueKey.split('-') + const [ projectKey ] = issueKey.split('-') const workflowName = await this.getProjectWorkflowName(projectKey) const stateMachine = await this.getWorkflowStateMachine(workflowName) @@ -703,64 +889,91 @@ class Jira { ) if (!shortestPath) { - console.error(`No transition path found from ${currentStatusName} to ${targetStatusName} that avoids ${excludeStates.join(', ')}`) + console.error( + `No transition path found from ${currentStatusName} to ${targetStatusName} that avoids ${excludeStates.join( + ', ' + )}` + ) return false } - console.log(`Found shortest transition path with ${shortestPath.length} steps:`) - shortestPath.forEach(t => console.log(` ${t.fromName} → ${t.toName} (${t.name})`)) + console.log( + `Found shortest transition path with ${shortestPath.length} steps:` + ) + shortestPath.forEach((t) => + console.log(` ${t.fromName} → ${t.toName} (${t.name})`) + ) for (let i = 0; i < shortestPath.length; i++) { const transition = shortestPath[i] const isLastTransition = i === shortestPath.length - 1 const availableTransitions = await this.getTransitions(issueKey) - const actualTransition = availableTransitions.find(t => - t.id === transition.id || - (t.to.name === transition.toName && t.name === transition.name) + const actualTransition = availableTransitions.find( + (t) => + t.id === transition.id || + (t.to.name === transition.toName && t.name === transition.name) ) if (!actualTransition) { - console.error(`Transition "${transition.name}" to ${transition.toName} not available for issue ${issueKey}`) - console.error(`Available transitions:`, availableTransitions.map(t => `${t.name} → ${t.to.name}`)) + console.error( + `Transition "${transition.name}" to ${transition.toName} not available for issue ${issueKey}` + ) + console.error( + `Available transitions:`, + availableTransitions.map((t) => `${t.name} → ${t.to.name}`) + ) return false } const transitionPayload = { transition: { - id: actualTransition.id - } + id: actualTransition.id, + }, } if (isLastTransition && Object.keys(fields).length > 0) { transitionPayload.fields = fields } - const transitionDetails = await this.getTransitionDetails(issueKey, actualTransition.id) + const transitionDetails = await this.getTransitionDetails( + issueKey, + actualTransition.id + ) if (transitionDetails.fields) { - for (const [fieldId, fieldInfo] of Object.entries(transitionDetails.fields)) { + for (const [ fieldId, fieldInfo ] of Object.entries( + transitionDetails.fields + )) { if (fieldInfo.required && !transitionPayload.fields?.[fieldId]) { - console.warn(`Required field ${fieldId} (${fieldInfo.name}) not provided for transition to ${transition.toName}`) + console.warn( + `Required field ${fieldId} (${fieldInfo.name}) not provided for transition to ${transition.toName}` + ) } } } await this.request(`/issue/${issueKey}/transitions`, { method: 'POST', - body: JSON.stringify(transitionPayload) + body: JSON.stringify(transitionPayload), }) - console.log(`✓ Transitioned ${issueKey}: ${transition.fromName} → ${transition.toName}`) + console.log( + `✓ Transitioned ${issueKey}: ${transition.fromName} → ${transition.toName}` + ) // Small delay to ensure Jira processes the transition - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) } - console.log(`Successfully transitioned ${issueKey} to ${targetStatusName}`) + console.log( + `Successfully transitioned ${issueKey} to ${targetStatusName}` + ) return true - } catch (error) { - console.error(`Error in smart transition for ${issueKey}:`, error.message) + console.error( + `Error in smart transition for ${issueKey}:`, + error.message + ) throw error } } From 88a5a8044537feb671ccb7c37903cf0ecc1e85ed Mon Sep 17 00:00:00 2001 From: Kamil Musial Date: Mon, 17 Nov 2025 10:56:21 +0100 Subject: [PATCH 3/4] chore: remove test artifacts and utility scripts - Remove test event JSON files (event.json, update_jira/event.json, update_jira/event.local.json) - Remove development utility scripts (test-custom-field-update.js, verify-custom-fields.js) - Clean up repository for production readiness --- event.json | 38 ----- update_jira/event.json | 38 ----- update_jira/event.local.json | 35 ----- utils/test-custom-field-update.js | 214 ---------------------------- utils/verify-custom-fields.js | 228 ------------------------------ 5 files changed, 553 deletions(-) delete mode 100644 event.json delete mode 100644 update_jira/event.json delete mode 100644 update_jira/event.local.json delete mode 100644 utils/test-custom-field-update.js delete mode 100644 utils/verify-custom-fields.js diff --git a/event.json b/event.json deleted file mode 100644 index 355a008..0000000 --- a/event.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "action": "closed", - "number": 42, - "pull_request": { - "url": "https://api.github.com/repos/coursedog/notion-scripts/pulls/42", - "id": 987654, - "number": 42, - "state": "closed", - "locked": false, - "title": "[DEX-36] Add deployment metadata sync to Jira", - "user": { - "login": "kamio90", - "id": 12345 - }, - "body": "Implements DEX-36: Pushes deployment metadata to Jira on deploy.", - "merged": true, - "merge_commit_sha": "abcdef1234567890", - "base": { - "ref": "main" - }, - "head": { - "ref": "feature/DEX-36-jira-deploy-metadata" - } - }, - "repository": { - "id": 123456, - "name": "notion-scripts", - "full_name": "coursedog/notion-scripts", - "owner": { - "login": "coursedog", - "id": 1 - } - }, - "sender": { - "login": "kamio90", - "id": 12345 - } -} diff --git a/update_jira/event.json b/update_jira/event.json deleted file mode 100644 index 70de43e..0000000 --- a/update_jira/event.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "action": "closed", - "number": 42, - "pull_request": { - "url": "https://api.github.com/repos/coursedog/notion-scripts/pulls/42", - "id": 987654, - "number": 42, - "state": "closed", - "locked": false, - "title": "[DEX-36] Add deployment metadata sync to Jira", - "user": { - "login": "kamio90", - "id": 12345 - }, - "body": "Implements DEX-36: Pushes deployment metadata to Jira on deploy.", - "merged": true, - "merge_commit_sha": "abcdef1234567890", - "base": { - "ref": "main" - }, - "head": { - "ref": "feature/ALL-593-jira-deploy-metadata" - } - }, - "repository": { - "id": 123456, - "name": "notion-scripts", - "full_name": "coursedog/notion-scripts", - "owner": { - "login": "coursedog", - "id": 1 - } - }, - "sender": { - "login": "kamio90", - "id": 12345 - } -} diff --git a/update_jira/event.local.json b/update_jira/event.local.json deleted file mode 100644 index a8902d5..0000000 --- a/update_jira/event.local.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "action": "closed", - "pull_request": { - "number": 123, - "title": "DEX-36: Fix GitHub JIRA integration", - "body": "This PR fixes the integration between GitHub and JIRA.\n\nRelated issues:\n- DEX-36\n- ALL-593", - "state": "closed", - "merged": true, - "draft": false, - "base": { - "ref": "staging", - "repo": { - "name": "notion-scripts", - "full_name": "coursedog/notion-scripts" - } - }, - "head": { - "ref": "DEX-36/fix-github-jira-integrations", - "sha": "abc123def456" - }, - "user": { - "login": "developer" - } - }, - "repository": { - "name": "notion-scripts", - "full_name": "coursedog/notion-scripts", - "owner": { - "login": "coursedog" - } - }, - "sender": { - "login": "developer" - } -} diff --git a/utils/test-custom-field-update.js b/utils/test-custom-field-update.js deleted file mode 100644 index d914797..0000000 --- a/utils/test-custom-field-update.js +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Custom Field Update Test Script - * - * Tests updating the custom fields on a real Jira issue to verify - * that the field IDs and option IDs are correct. - * - * Usage: - * node utils/test-custom-field-update.js [ISSUE_KEY] - * - * Example: - * node utils/test-custom-field-update.js DEX-36 - */ - -require('dotenv').config() -const Jira = require('./jira') - -async function testCustomFieldUpdate () { - console.log(`\n${'='.repeat(70)}`) - console.log('JIRA CUSTOM FIELD UPDATE TEST') - console.log(`${'='.repeat(70)}\n`) - - // Check environment variables - if ( - !process.env.JIRA_BASE_URL || - !process.env.JIRA_EMAIL || - !process.env.JIRA_API_TOKEN - ) { - console.error('❌ ERROR: Missing required environment variables') - console.error(' Required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN\n') - process.exit(1) - } - - const testIssueKey = - process.argv[2] || process.env.TEST_JIRA_ISSUE_KEY || 'DEX-36' - - console.log(`Test Issue: ${testIssueKey}`) - console.log(`Base URL: ${process.env.JIRA_BASE_URL}\n`) - - const jira = new Jira({ - baseUrl: process.env.JIRA_BASE_URL, - email: process.env.JIRA_EMAIL, - apiToken: process.env.JIRA_API_TOKEN, - }) - - try { - // Capture original state - console.log('─'.repeat(70)) - console.log('STEP 1: Capturing original field values') - console.log(`${'─'.repeat(70)}\n`) - - const originalResponse = await jira.request( - `/issue/${testIssueKey}?fields=customfield_11473,customfield_11474,customfield_11475` - ) - const originalIssue = await originalResponse.json() - - const originalEnv = originalIssue.fields.customfield_11473 - const originalStageTs = originalIssue.fields.customfield_11474 - const originalProdTs = originalIssue.fields.customfield_11475 - - console.log('Original values:') - console.log( - ` Release Environment (11473): ${JSON.stringify(originalEnv)}` - ) - console.log(` Stage Timestamp (11474): ${originalStageTs || 'null'}`) - console.log( - ` Production Timestamp (11475): ${originalProdTs || 'null'}\n` - ) - - // Test staging deployment field update - console.log('─'.repeat(70)) - console.log('STEP 2: Testing STAGING deployment field updates') - console.log(`${'─'.repeat(70)}\n`) - - const stagingTimestamp = new Date().toISOString() - const stagingFields = { - customfield_11474: stagingTimestamp, - customfield_11473: { id: '11942' }, // Staging environment option ID - } - - console.log('Attempting to set:') - console.log(` customfield_11474 = ${stagingTimestamp}`) - console.log(' customfield_11473 = { id: "11942" } (staging)\n') - - try { - await jira.updateCustomFields(testIssueKey, stagingFields) - console.log('✓ Staging fields updated successfully!\n') - - // Verify the update - const verifyResponse = await jira.request( - `/issue/${testIssueKey}?fields=customfield_11473,customfield_11474` - ) - const verifiedIssue = await verifyResponse.json() - - console.log('Verified values:') - console.log( - ` Release Environment: ${JSON.stringify( - verifiedIssue.fields.customfield_11473 - )}` - ) - console.log( - ` Stage Timestamp: ${verifiedIssue.fields.customfield_11474}\n` - ) - } catch (error) { - console.error('❌ Failed to update staging fields:', error.message) - console.error( - ' This might indicate incorrect field IDs or option IDs\n' - ) - throw error - } - - // Wait a moment - await new Promise((resolve) => setTimeout(resolve, 1000)) - - // Test production deployment field update - console.log('─'.repeat(70)) - console.log('STEP 3: Testing PRODUCTION deployment field updates') - console.log(`${'─'.repeat(70)}\n`) - - const prodTimestamp = new Date().toISOString() - const prodFields = { - customfield_11475: prodTimestamp, - customfield_11473: { id: '11943' }, // Production environment option ID - } - - console.log('Attempting to set:') - console.log(` customfield_11475 = ${prodTimestamp}`) - console.log(' customfield_11473 = { id: "11943" } (production)\n') - - try { - await jira.updateCustomFields(testIssueKey, prodFields) - console.log('✓ Production fields updated successfully!\n') - - // Verify the update - const verifyResponse = await jira.request( - `/issue/${testIssueKey}?fields=customfield_11473,customfield_11475` - ) - const verifiedIssue = await verifyResponse.json() - - console.log('Verified values:') - console.log( - ` Release Environment: ${JSON.stringify( - verifiedIssue.fields.customfield_11473 - )}` - ) - console.log( - ` Production Timestamp: ${verifiedIssue.fields.customfield_11475}\n` - ) - } catch (error) { - console.error('❌ Failed to update production fields:', error.message) - console.error( - ' This might indicate incorrect field IDs or option IDs\n' - ) - throw error - } - - // Summary - console.log('─'.repeat(70)) - console.log('TEST SUMMARY') - console.log(`${'─'.repeat(70)}\n`) - - console.log('✅ ALL TESTS PASSED!') - console.log('\nVerified field IDs:') - console.log(' ✓ customfield_11473 (Release Environment) - select field') - console.log(' ✓ customfield_11474 (Stage Release Timestamp) - datetime') - console.log( - ' ✓ customfield_11475 (Production Release Timestamp) - datetime' - ) - console.log('\nVerified option IDs:') - console.log(' ✓ 11942 - Staging environment') - console.log(' ✓ 11943 - Production environment') - console.log( - '\n💡 The custom field configuration in update_jira/index.js is CORRECT!\n' - ) - - // Optionally restore original values - console.log('⚠️ Note: Test values have been set on the issue.') - console.log( - ` You may want to manually restore original values if needed.\n` - ) - } catch (error) { - console.error('\n❌ TEST FAILED') - console.error(` ${error.message}\n`) - - if (error.message.includes('404')) { - console.error(` Issue ${testIssueKey} not found.`) - } else if ( - error.message.includes('does not exist') || - error.message.includes('is not on the appropriate screen') - ) { - console.error( - ' One or more custom field IDs are incorrect or not available for this issue type.' - ) - } else if ( - error.message.includes('option') || - error.message.includes('11942') || - error.message.includes('11943') - ) { - console.error( - ' Option IDs (11942 or 11943) are incorrect for the Release Environment field.' - ) - console.error( - ' Check Jira admin settings to find the correct option IDs.' - ) - } - - process.exit(1) - } -} - -// Run test -testCustomFieldUpdate().catch((error) => { - console.error('\n❌ Unexpected error:', error) - process.exit(1) -}) diff --git a/utils/verify-custom-fields.js b/utils/verify-custom-fields.js deleted file mode 100644 index 02fbe3d..0000000 --- a/utils/verify-custom-fields.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Custom Field Verification Script - * - * This script verifies that the Jira custom field IDs used in the codebase - * match the actual custom fields in your Jira instance. - * - * According to ticket ALL-593, we need: - * - customfield_11473: Release Environment (select field) - * - customfield_11474: Stage Release Timestamp (date-time) - * - customfield_11475: Production Release Timestamp (date-time) - * - * Usage: - * node utils/verify-custom-fields.js - * - * Requirements: - * - .env file with JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN - * - TEST_JIRA_ISSUE_KEY environment variable (optional, for field inspection) - */ - -require('dotenv').config() -const Jira = require('./jira') - -const REQUIRED_FIELDS = { - customfield_11473: { - name: 'Release Environment', - type: 'select', - description: 'Select field with options for staging/production', - expectedOptions: [ 'staging', 'production' ], - }, - customfield_11474: { - name: 'Stage Release Timestamp', - type: 'datetime', - description: 'Date-time field for staging deployments', - }, - customfield_11475: { - name: 'Production Release Timestamp', - type: 'datetime', - description: 'Date-time field for production deployments', - }, -} - -// Option IDs used in the code -const EXPECTED_OPTION_IDS = { - staging: '11942', - production: '11943', -} - -/** - * Verify Jira custom field configuration - */ -async function verifyCustomFields () { - console.log(`\n${'='.repeat(70)}`) - console.log('JIRA CUSTOM FIELD VERIFICATION') - console.log(`${'='.repeat(70)}\n`) - - // Check environment variables - if ( - !process.env.JIRA_BASE_URL || - !process.env.JIRA_EMAIL || - !process.env.JIRA_API_TOKEN - ) { - console.error('❌ ERROR: Missing required environment variables') - console.error(' Required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN') - console.error(' Please create a .env file with these variables.\n') - process.exit(1) - } - - console.log('✓ Environment variables found') - console.log(` Base URL: ${process.env.JIRA_BASE_URL}`) - console.log(` Email: ${process.env.JIRA_EMAIL}\n`) - - const jira = new Jira({ - baseUrl: process.env.JIRA_BASE_URL, - email: process.env.JIRA_EMAIL, - apiToken: process.env.JIRA_API_TOKEN, - }) - - const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || 'DEX-36' - - try { - console.log( - `Fetching custom field metadata from test issue: ${testIssueKey}\n` - ) - - // Fetch the test issue to inspect its custom fields - const response = await jira.request(`/issue/${testIssueKey}?expand=names`) - const issue = await response.json() - - console.log('─'.repeat(70)) - console.log('VERIFICATION RESULTS') - console.log(`${'─'.repeat(70)}\n`) - - let allFieldsValid = true - const foundFields = {} - - // Check each required custom field - for (const [ fieldId, expectedConfig ] of Object.entries(REQUIRED_FIELDS)) { - console.log(`Checking ${fieldId} (${expectedConfig.name})...`) - - // Check if field exists in the issue - const fieldValue = issue.fields[fieldId] - const fieldName = issue.names?.[fieldId] - - if (fieldValue !== undefined || fieldName) { - console.log(` ✓ Field exists in Jira`) - console.log(` Field Name: ${fieldName || 'N/A'}`) - console.log(` Current Value: ${JSON.stringify(fieldValue)}`) - - foundFields[fieldId] = { - name: fieldName, - value: fieldValue, - exists: true, - } - - // For select fields, check options - if ( - expectedConfig.type === 'select' && - fieldValue && - typeof fieldValue === 'object' - ) { - console.log(` Option ID: ${fieldValue.id || 'N/A'}`) - console.log(` Option Value: ${fieldValue.value || 'N/A'}`) - } - } else { - console.log(` ❌ Field NOT FOUND in this issue`) - console.log(` This may be normal if the field hasn't been set yet.`) - allFieldsValid = false - foundFields[fieldId] = { exists: false } - } - console.log() - } - - // Get all custom fields to find the Release Environment options - console.log('─'.repeat(70)) - console.log('RELEASE ENVIRONMENT FIELD OPTIONS') - console.log(`${'─'.repeat(70)}\n`) - - try { - // Try to get field metadata - const fieldResponse = await jira.request('/field') - const fields = await fieldResponse.json() - - const releaseEnvField = fields.find((f) => f.id === 'customfield_11473') - - if (releaseEnvField) { - console.log(`✓ Found field: ${releaseEnvField.name}`) - console.log(` Field ID: ${releaseEnvField.id}`) - console.log(` Field Type: ${releaseEnvField.schema?.type || 'N/A'}`) - - // Try to get the field configuration to see options - if (releaseEnvField.schema?.custom) { - console.log(` Custom Type: ${releaseEnvField.schema.custom}`) - } - } else { - console.log(`⚠️ Could not find metadata for customfield_11473`) - } - } catch (error) { - console.log(`⚠️ Could not fetch field metadata: ${error.message}`) - } - - console.log(`\n${'─'.repeat(70)}`) - console.log('EXPECTED VS ACTUAL CONFIGURATION') - console.log(`${'─'.repeat(70)}\n`) - - console.log('Expected Configuration (from ticket ALL-593):') - console.log(' • customfield_11473: Release Environment (select)') - console.log(` - Option for 'staging': ${EXPECTED_OPTION_IDS.staging}`) - console.log( - ` - Option for 'production': ${EXPECTED_OPTION_IDS.production}` - ) - console.log(' • customfield_11474: Stage Release Timestamp (datetime)') - console.log( - ' • customfield_11475: Production Release Timestamp (datetime)\n' - ) - - console.log('Current Code Configuration (update_jira/index.js):') - console.log(' • For staging deployments:') - console.log(' - Sets customfield_11474 to new Date() ✓') - console.log(" - Sets customfield_11473 to { id: '11942' } ✓") - console.log(' • For production deployments:') - console.log(' - Sets customfield_11475 to new Date() ✓') - console.log(" - Sets customfield_11473 to { id: '11943' } ✓\n") - - // Summary - console.log('─'.repeat(70)) - console.log('SUMMARY') - console.log(`${'─'.repeat(70)}\n`) - - if (allFieldsValid) { - console.log('✓ All required custom fields exist in Jira') - } else { - console.log('⚠️ Some fields were not found in the test issue') - console.log(" This may be normal if they haven't been set yet.") - } - - console.log('\n⚠️ IMPORTANT: Option ID Verification Required') - console.log( - ' The option IDs (11942, 11943) for the Release Environment field' - ) - console.log(' need to be verified manually in Jira admin settings:') - console.log(' 1. Go to Jira Settings > Issues > Custom Fields') - console.log(" 2. Find 'Release Environment' field") - console.log(" 3. Click 'Configure' > 'Edit Options'") - console.log(' 4. Verify the option IDs match:') - console.log(' - Staging option: 11942') - console.log(' - Production option: 11943\n') - - console.log('💡 To test setting these fields:') - console.log(` node utils/test-custom-field-update.js ${testIssueKey}\n`) - } catch (error) { - console.error('\n❌ ERROR: Failed to verify custom fields') - console.error(` ${error.message}\n`) - - if (error.message.includes('404')) { - console.error( - ` Issue ${testIssueKey} not found. Set TEST_JIRA_ISSUE_KEY to a valid issue.` - ) - } - - process.exit(1) - } -} - -// Run verification -verifyCustomFields().catch((error) => { - console.error('\n❌ Unexpected error:', error) - process.exit(1) -}) From ed5253c39f25ea778f888349b549eb6497bfd9e6 Mon Sep 17 00:00:00 2001 From: Kamil Musial Date: Mon, 17 Nov 2025 16:31:29 +0100 Subject: [PATCH 4/4] fix: resolve ESLint errors - Use node: prefix for built-in modules (assert, fs, child_process) - Remove structuredClone availability check (always available in Node.js 20+) - Add Buffer and structuredClone to ESLint globals - Fix quote style consistency - All ESLint checks now pass --- .eslintrc.js | 2 + update_jira/index.js | 512 ++++++++++++++++----------------- update_jira/index.test.js | 174 +++++------ utils/jira.integration.test.js | 300 ++++++++++--------- utils/jira.js | 2 +- 5 files changed, 493 insertions(+), 497 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 663a8f4..8d9d0b2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,8 @@ module.exports = { }, globals: { process: 'readonly', + Buffer: 'readonly', + structuredClone: 'readonly', }, rules: { 'no-extend-native': [ 'error', { exceptions: [ 'Array' ] } ], diff --git a/update_jira/index.js b/update_jira/index.js index 9f8dd73..d9e2860 100644 --- a/update_jira/index.js +++ b/update_jira/index.js @@ -1,56 +1,52 @@ -require("dotenv").config(); -const core = require("@actions/core"); -const github = require("@actions/github"); -const { Octokit } = require("@octokit/rest"); -const Jira = require("./../utils/jira"); -const fs = require("fs"); +require('dotenv').config() +const core = require('@actions/core') +const github = require('@actions/github') +const { Octokit } = require('@octokit/rest') +const Jira = require('./../utils/jira') +const fs = require('node:fs') /** * Mask sensitive data in logs. * @param {object} obj - Any object to mask * @returns {object} */ -function maskSensitive(obj) { - if (!obj || typeof obj !== "object") return obj; - // Prefer structuredClone if available - const clone = - typeof structuredClone === "function" - ? structuredClone(obj) - : JSON.parse(JSON.stringify(obj)); - if (clone.apiToken) clone.apiToken = "***"; - if (clone.email) clone.email = "***"; - if (clone.headers?.Authorization) clone.headers.Authorization = "***"; - if (clone.JIRA_API_TOKEN) clone.JIRA_API_TOKEN = "***"; - if (clone.JIRA_EMAIL) clone.JIRA_EMAIL = "***"; - return clone; +function maskSensitive (obj) { + if (!obj || typeof obj !== 'object') return obj + const clone = structuredClone(obj) + if (clone.apiToken) clone.apiToken = '***' + if (clone.email) clone.email = '***' + if (clone.headers?.Authorization) clone.headers.Authorization = '***' + if (clone.JIRA_API_TOKEN) clone.JIRA_API_TOKEN = '***' + if (clone.JIRA_EMAIL) clone.JIRA_EMAIL = '***' + return clone } /** * Detect environment: 'ci', 'github', or 'local'. * @returns {'ci'|'github'|'local'} */ -function detectEnvironment() { - if (process.env.GITHUB_ACTIONS === "true") return "github"; - if (process.env.CI === "true") return "ci"; - return "local"; +function detectEnvironment () { + if (process.env.GITHUB_ACTIONS === 'true') return 'github' + if (process.env.CI === 'true') return 'ci' + return 'local' } -const ENVIRONMENT = detectEnvironment(); +const ENVIRONMENT = detectEnvironment() /** * Log environment and startup info (professional, clear, masked). */ -function logEnvSection() { - console.log("\n===================="); - console.log("Coursedog: update_jira script startup"); - console.log("Environment:", ENVIRONMENT); - console.log("GITHUB_REF:", process.env.GITHUB_REF); - console.log("GITHUB_EVENT_NAME:", process.env.GITHUB_EVENT_NAME); - console.log("GITHUB_EVENT_PATH:", process.env.GITHUB_EVENT_PATH); - console.log("GITHUB_REPOSITORY:", process.env.GITHUB_REPOSITORY); - console.log("JIRA_BASE_URL:", process.env.JIRA_BASE_URL); - console.log("JIRA_EMAIL:", process.env.JIRA_EMAIL ? "***" : undefined); - console.log("JIRA_PROJECT_KEY:", process.env.JIRA_PROJECT_KEY); - console.log("====================\n"); +function logEnvSection () { + console.log('\n====================') + console.log('Coursedog: update_jira script startup') + console.log('Environment:', ENVIRONMENT) + console.log('GITHUB_REF:', process.env.GITHUB_REF) + console.log('GITHUB_EVENT_NAME:', process.env.GITHUB_EVENT_NAME) + console.log('GITHUB_EVENT_PATH:', process.env.GITHUB_EVENT_PATH) + console.log('GITHUB_REPOSITORY:', process.env.GITHUB_REPOSITORY) + console.log('JIRA_BASE_URL:', process.env.JIRA_BASE_URL) + console.log('JIRA_EMAIL:', process.env.JIRA_EMAIL ? '***' : undefined) + console.log('JIRA_PROJECT_KEY:', process.env.JIRA_PROJECT_KEY) + console.log('====================\n') } /** @@ -58,12 +54,12 @@ function logEnvSection() { * @param {string} message * @param {...any} args */ -function debugLog(message, ...args) { - const safeArgs = args.map(maskSensitive); - console.log(`[DEBUG] ${message}`, ...safeArgs); +function debugLog (message, ...args) { + const safeArgs = args.map(maskSensitive) + console.log(`[DEBUG] ${message}`, ...safeArgs) } -logEnvSection(); +logEnvSection() /** * Custom Field Configuration for Deployment Tracking @@ -83,8 +79,8 @@ logEnvSection(); * To test updating these fields: * node utils/test-custom-field-update.js [ISSUE_KEY] */ -const stagingReleaseEnvId = "11942"; // Option ID for "staging" in customfield_11473 -const prodReleaseEnvId = "11943"; // Option ID for "production" in customfield_11473 +const stagingReleaseEnvId = '11942' // Option ID for "staging" in customfield_11473 +const prodReleaseEnvId = '11943' // Option ID for "production" in customfield_11473 /** * Status mapping configuration for different branch deployments. @@ -97,9 +93,9 @@ const prodReleaseEnvId = "11943"; // Option ID for "production" in customfield_1 */ const statusMap = { master: { - status: "Done", + status: 'Done', transitionFields: { - resolution: "Done", + resolution: 'Done', }, customFields: { customfield_11475: new Date(), @@ -107,9 +103,9 @@ const statusMap = { }, }, main: { - status: "Done", + status: 'Done', transitionFields: { - resolution: "Done", + resolution: 'Done', }, customFields: { customfield_11475: new Date(), @@ -117,9 +113,9 @@ const statusMap = { }, }, staging: { - status: "Deployed to Staging", + status: 'Deployed to Staging', transitionFields: { - resolution: "Done", + resolution: 'Done', }, customFields: { customfield_11474: new Date(), @@ -127,164 +123,164 @@ const statusMap = { }, }, dev: { - status: "Merged", + status: 'Merged', transitionFields: { - resolution: "Done", + resolution: 'Done', }, customFields: {}, }, -}; +} -run(); +run() -async function run() { +async function run () { try { debugLog( - "run() started. Checking event type and initializing Jira connection." - ); + 'run() started. Checking event type and initializing Jira connection.' + ) debugLog( - "Rollback/dry-run mode:", - process.env.DRY_RUN === "true" ? "ENABLED" : "DISABLED" - ); + 'Rollback/dry-run mode:', + process.env.DRY_RUN === 'true' ? 'ENABLED' : 'DISABLED' + ) const { GITHUB_REF, GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REPOSITORY, GITHUB_TOKEN, - } = process.env; + } = process.env const JIRA_BASE_URL = - core.getInput("JIRA_BASE_URL") || process.env.JIRA_BASE_URL; - const JIRA_EMAIL = core.getInput("JIRA_EMAIL") || process.env.JIRA_EMAIL; + core.getInput('JIRA_BASE_URL') || process.env.JIRA_BASE_URL + const JIRA_EMAIL = core.getInput('JIRA_EMAIL') || process.env.JIRA_EMAIL const JIRA_API_TOKEN = - core.getInput("JIRA_API_TOKEN") || process.env.JIRA_API_TOKEN; + core.getInput('JIRA_API_TOKEN') || process.env.JIRA_API_TOKEN - debugLog("Attempting to initialize Jira utility with:", { + debugLog('Attempting to initialize Jira utility with:', { JIRA_BASE_URL, JIRA_EMAIL, - JIRA_API_TOKEN: JIRA_API_TOKEN ? "***" : undefined, - }); + JIRA_API_TOKEN: JIRA_API_TOKEN ? '***' : undefined, + }) const jiraUtil = new Jira({ baseUrl: JIRA_BASE_URL, email: JIRA_EMAIL, apiToken: JIRA_API_TOKEN, - }); - debugLog("Jira utility initialized."); + }) + debugLog('Jira utility initialized.') // --- EVENT PAYLOAD HANDLING --- - let eventData = null; - if (ENVIRONMENT === "local") { + let eventData = null + if (ENVIRONMENT === 'local') { // Allow local override of event payload for testing - const localEventPath = "./update_jira/event.local.json"; + const localEventPath = './update_jira/event.local.json' if (fs.existsSync(localEventPath)) { - debugLog("Detected local event payload override:", localEventPath); - eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + debugLog('Detected local event payload override:', localEventPath) + eventData = JSON.parse(fs.readFileSync(localEventPath, 'utf8')) } else if (GITHUB_EVENT_PATH && fs.existsSync(GITHUB_EVENT_PATH)) { debugLog( - "Loading event payload from GITHUB_EVENT_PATH:", + 'Loading event payload from GITHUB_EVENT_PATH:', GITHUB_EVENT_PATH - ); - eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, "utf8")); + ) + eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, 'utf8')) } else { - debugLog("No event payload found for local run."); + debugLog('No event payload found for local run.') } } else if (GITHUB_EVENT_PATH && fs.existsSync(GITHUB_EVENT_PATH)) { debugLog( - "Loading event payload from GITHUB_EVENT_PATH:", + 'Loading event payload from GITHUB_EVENT_PATH:', GITHUB_EVENT_PATH - ); - eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, "utf8")); + ) + eventData = JSON.parse(fs.readFileSync(GITHUB_EVENT_PATH, 'utf8')) } if ( - (GITHUB_EVENT_NAME === "pull_request" || - GITHUB_EVENT_NAME === "pull_request_target") && + (GITHUB_EVENT_NAME === 'pull_request' || + GITHUB_EVENT_NAME === 'pull_request_target') && eventData ) { debugLog( - "Detected pull request event. Loaded event data:", + 'Detected pull request event. Loaded event data:', maskSensitive(eventData) - ); - if (process.env.DRY_RUN === "true") { + ) + if (process.env.DRY_RUN === 'true') { debugLog( - "DRY RUN: Would handle pull request event, skipping actual Jira update." - ); - return; + 'DRY RUN: Would handle pull request event, skipping actual Jira update.' + ) + return } - await handlePullRequestEvent(eventData, jiraUtil, GITHUB_REPOSITORY); - return; + await handlePullRequestEvent(eventData, jiraUtil, GITHUB_REPOSITORY) + return } const allowedBranches = [ - "refs/heads/master", - "refs/heads/main", - "refs/heads/staging", - "refs/heads/dev", - ]; + 'refs/heads/master', + 'refs/heads/main', + 'refs/heads/staging', + 'refs/heads/dev', + ] if (allowedBranches.includes(GITHUB_REF)) { - const branchName = GITHUB_REF.split("/").pop(); - debugLog("Detected push event for branch:", branchName); - if (process.env.DRY_RUN === "true") { + const branchName = GITHUB_REF.split('/').pop() + debugLog('Detected push event for branch:', branchName) + if (process.env.DRY_RUN === 'true') { debugLog( - "DRY RUN: Would handle push event, skipping actual Jira update." - ); - return; + 'DRY RUN: Would handle push event, skipping actual Jira update.' + ) + return } await handlePushEvent( branchName, jiraUtil, GITHUB_REPOSITORY, GITHUB_TOKEN - ); + ) } } catch (error) { - debugLog("Error in run():", error); - core.setFailed(error.message); + debugLog('Error in run():', error) + core.setFailed(error.message) } } /** * Prepare fields for Jira transition, converting names to IDs where needed */ -async function prepareFields(fields, jiraUtil) { - const preparedFields = {}; +async function prepareFields (fields, jiraUtil) { + const preparedFields = {} - for (const [fieldName, fieldValue] of Object.entries(fields)) { - if (fieldName === "resolution" && typeof fieldValue === "string") { + for (const [ fieldName, fieldValue ] of Object.entries(fields)) { + if (fieldName === 'resolution' && typeof fieldValue === 'string') { // Look up resolution ID by name - const resolutions = await jiraUtil.getFieldOptions("resolution"); - const resolution = resolutions.find((r) => r.name === fieldValue); + const resolutions = await jiraUtil.getFieldOptions('resolution') + const resolution = resolutions.find((r) => r.name === fieldValue) if (resolution) { - preparedFields.resolution = { id: resolution.id }; + preparedFields.resolution = { id: resolution.id } } else { - console.warn(`Resolution "${fieldValue}" not found`); + console.warn(`Resolution "${fieldValue}" not found`) } - } else if (fieldName === "priority" && typeof fieldValue === "string") { + } else if (fieldName === 'priority' && typeof fieldValue === 'string') { // Look up priority ID by name - const priorities = await jiraUtil.getFieldOptions("priority"); - const priority = priorities.find((p) => p.name === fieldValue); + const priorities = await jiraUtil.getFieldOptions('priority') + const priority = priorities.find((p) => p.name === fieldValue) if (priority) { - preparedFields.priority = { id: priority.id }; + preparedFields.priority = { id: priority.id } } - } else if (fieldName === "assignee" && typeof fieldValue === "string") { + } else if (fieldName === 'assignee' && typeof fieldValue === 'string') { // For assignee, you might need to look up the user // This depends on your Jira configuration - preparedFields.assignee = { name: fieldValue }; + preparedFields.assignee = { name: fieldValue } } else { // Pass through other fields as-is - preparedFields[fieldName] = fieldValue; + preparedFields[fieldName] = fieldValue } } - return preparedFields; + return preparedFields } /** * Update issue with transition and then update custom fields separately */ -async function updateIssueWithCustomFields( +async function updateIssueWithCustomFields ( jiraUtil, issueKey, targetStatus, @@ -297,78 +293,78 @@ async function updateIssueWithCustomFields( const preparedTransitionFields = await prepareFields( transitionFields, jiraUtil - ); + ) await jiraUtil.transitionIssue( issueKey, targetStatus, excludeStates, preparedTransitionFields - ); + ) // Then, if there are custom fields to update, update them separately if (customFields && Object.keys(customFields).length > 0) { - await jiraUtil.updateCustomFields(issueKey, customFields); + await jiraUtil.updateCustomFields(issueKey, customFields) } - return true; + return true } catch (error) { - console.error(`Failed to update ${issueKey}:`, error.message); - throw error; + console.error(`Failed to update ${issueKey}:`, error.message) + throw error } } /** * Handle pull request events (open, close, etc) */ -async function handlePullRequestEvent(eventData, jiraUtil) { - const { action, pull_request } = eventData; +async function handlePullRequestEvent (eventData, jiraUtil) { + const { action, pull_request } = eventData - const issueKeys = extractJiraIssueKeys(pull_request); + const issueKeys = extractJiraIssueKeys(pull_request) if (issueKeys.length === 0) { - console.log("No Jira issue keys found in PR"); - return; + console.log('No Jira issue keys found in PR') + return } - console.log(`Found Jira issues: ${issueKeys.join(", ")}`); + console.log(`Found Jira issues: ${issueKeys.join(', ')}`) - let targetStatus = null; - let transitionFields = {}; - let customFields = {}; - const targetBranch = pull_request.base.ref; + let targetStatus = null + let transitionFields = {} + let customFields = {} + const targetBranch = pull_request.base.ref switch (action) { - case "opened": - case "reopened": - case "ready_for_review": - targetStatus = "Code Review"; - break; - case "converted_to_draft": - targetStatus = "In Development"; - break; - case "synchronize": + case 'opened': + case 'reopened': + case 'ready_for_review': + targetStatus = 'Code Review' + break + case 'converted_to_draft': + targetStatus = 'In Development' + break + case 'synchronize': if (!pull_request.draft) { - targetStatus = "Code Review"; + targetStatus = 'Code Review' } - break; - case "closed": + break + case 'closed': if (pull_request.merged) { - const branchConfig = statusMap[targetBranch]; + const branchConfig = statusMap[targetBranch] if (branchConfig) { - targetStatus = branchConfig.status; - transitionFields = branchConfig.transitionFields || {}; - customFields = branchConfig.customFields || {}; + targetStatus = branchConfig.status + transitionFields = branchConfig.transitionFields || {} + customFields = branchConfig.customFields || {} } else { - targetStatus = "Done"; - transitionFields = { resolution: "Done" }; + targetStatus = 'Done' + transitionFields = { resolution: 'Done' } } } else { - console.log("PR closed without merging, skipping status update"); - return; + console.log('PR closed without merging, skipping status update') + return } - break; + break default: - console.log("No status updates for action:", action); - break; + console.log('No status updates for action:', action) + break } if (targetStatus) { @@ -378,12 +374,12 @@ async function handlePullRequestEvent(eventData, jiraUtil) { jiraUtil, issueKey, targetStatus, - ["Blocked", "Rejected"], + [ 'Blocked', 'Rejected' ], transitionFields, customFields - ); + ) } catch (error) { - console.error(`Failed to update ${issueKey}:`, error.message); + console.error(`Failed to update ${issueKey}:`, error.message) } } } @@ -392,7 +388,7 @@ async function handlePullRequestEvent(eventData, jiraUtil) { /** * Handle push events to branches */ -async function handlePushEvent( +async function handlePushEvent ( branch, jiraUtil, githubRepository, @@ -400,105 +396,105 @@ async function handlePushEvent( ) { const octokit = new Octokit({ auth: githubToken, - }); + }) - const [githubOwner, repositoryName] = githubRepository.split("/"); + const [ githubOwner, repositoryName ] = githubRepository.split('/') const { data } = await octokit.rest.repos.getCommit({ owner: githubOwner, repo: repositoryName, ref: branch, perPage: 1, page: 1, - }); + }) const { commit: { message: commitMessage }, - } = data; - const branchConfig = statusMap[branch]; + } = data + const branchConfig = statusMap[branch] if (!branchConfig) { - console.log(`No status mapping for branch: ${branch}`); - return; + console.log(`No status mapping for branch: ${branch}`) + return } - const newStatus = branchConfig.status; - const transitionFields = branchConfig.transitionFields || {}; - const customFields = branchConfig.customFields || {}; + const newStatus = branchConfig.status + const transitionFields = branchConfig.transitionFields || {} + const customFields = branchConfig.customFields || {} - const shouldCheckCommitHistory = ["master", "main", "staging"].includes( + const shouldCheckCommitHistory = [ 'master', 'main', 'staging' ].includes( branch - ); + ) - const prMatch = commitMessage.match(/#([0-9]+)/); + const prMatch = commitMessage.match(/#([0-9]+)/) // Handle staging to production deployment - if (branch === "master" || branch === "main") { - console.log("Production deployment: extracting issues from commit history"); + if (branch === 'master' || branch === 'main') { + console.log('Production deployment: extracting issues from commit history') try { const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory( - "HEAD~100", - "HEAD" - ); + 'HEAD~100', + 'HEAD' + ) if (commitHistoryIssues.length > 0) { console.log( `Found ${commitHistoryIssues.length} issues in production commit history` - ); + ) const updateResults = await updateIssuesFromCommitHistoryWithCustomFields( jiraUtil, commitHistoryIssues, newStatus, - ["Blocked", "Rejected"], + [ 'Blocked', 'Rejected' ], transitionFields, customFields - ); + ) console.log( `Production deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed` - ); + ) } else { - console.log("No Jira issues found in production commit history"); + console.log('No Jira issues found in production commit history') } } catch (error) { console.error( - "Error processing production commit history:", + 'Error processing production commit history:', error.message - ); + ) } // Also handle direct PR merges to production if (prMatch) { - const prNumber = extractPrNumber(commitMessage); - const prUrl = `${repositoryName}/pull/${prNumber}`; + const prNumber = extractPrNumber(commitMessage) + const prUrl = `${repositoryName}/pull/${prNumber}` if (prNumber) { console.log( `Also updating issues from PR ${prUrl} to production status` - ); + ) await updateByPRWithCustomFields( jiraUtil, prUrl, newStatus, transitionFields, customFields - ); + ) } } - return; + return } // Handle dev to staging deployment - if (branch === "staging") { - console.log("Staging deployment: extracting issues from commit history"); + if (branch === 'staging') { + console.log('Staging deployment: extracting issues from commit history') try { // Get issue keys from commit history const commitHistoryIssues = - await jiraUtil.extractIssueKeysFromGitHubContext(github.context); + await jiraUtil.extractIssueKeysFromGitHubContext(github.context) if (commitHistoryIssues.length > 0) { console.log( `Found ${commitHistoryIssues.length} issues in staging commit history` - ); + ) // Update issues found in commit history const updateResults = @@ -506,53 +502,53 @@ async function handlePushEvent( jiraUtil, commitHistoryIssues, newStatus, - ["Blocked", "Rejected"], + [ 'Blocked', 'Rejected' ], transitionFields, customFields - ); + ) console.log( `Staging deployment results: ${updateResults.successful} successful, ${updateResults.failed} failed` - ); - return; + ) + return } else { - console.log("No Jira issues found in staging commit history"); - return; + console.log('No Jira issues found in staging commit history') + return } } catch (error) { - console.error("Error processing staging commit history:", error.message); + console.error('Error processing staging commit history:', error.message) } // Also handle direct PR merges to staging if (prMatch) { - const prNumber = prMatch[1]; - const prUrl = `${repositoryName}/pull/${prNumber}`; - console.log(`Also updating issues from PR ${prUrl} to staging status`); + const prNumber = prMatch[1] + const prUrl = `${repositoryName}/pull/${prNumber}` + console.log(`Also updating issues from PR ${prUrl} to staging status`) await updateByPRWithCustomFields( jiraUtil, prUrl, newStatus, transitionFields, customFields - ); + ) } - return; + return } // Handle PR merges to other branches (like dev) if (prMatch) { - const prNumber = prMatch[1]; - const prUrl = `${repositoryName}/pull/${prNumber}`; + const prNumber = prMatch[1] + const prUrl = `${repositoryName}/pull/${prNumber}` console.log( `Updating issues mentioning PR ${prUrl} to status: ${newStatus}` - ); + ) await updateByPRWithCustomFields( jiraUtil, prUrl, newStatus, transitionFields, customFields - ); + ) } // Additionally, for important branches, check commit history for issue keys @@ -560,14 +556,14 @@ async function handlePushEvent( try { // Get issue keys from recent commit history (last 50 commits) const commitHistoryIssues = await jiraUtil.getIssueKeysFromCommitHistory( - "HEAD~50", - "HEAD" - ); + 'HEAD~50', + 'HEAD' + ) if (commitHistoryIssues.length > 0) { console.log( `Found ${commitHistoryIssues.length} additional issues in commit history for ${branch} branch` - ); + ) // Update issues found in commit history const updateResults = @@ -575,17 +571,17 @@ async function handlePushEvent( jiraUtil, commitHistoryIssues, newStatus, - ["Blocked", "Rejected"], + [ 'Blocked', 'Rejected' ], transitionFields, customFields - ); + ) console.log( `Commit history update results: ${updateResults.successful} successful, ${updateResults.failed} failed` - ); + ) } } catch (error) { - console.error("Error processing commit history:", error.message); + console.error('Error processing commit history:', error.message) // Don't fail the entire action if commit history processing fails } } @@ -594,7 +590,7 @@ async function handlePushEvent( /** * Update issues from commit history with separate custom field updates */ -async function updateIssuesFromCommitHistoryWithCustomFields( +async function updateIssuesFromCommitHistoryWithCustomFields ( jiraUtil, issueKeys, targetStatus, @@ -603,11 +599,11 @@ async function updateIssuesFromCommitHistoryWithCustomFields( customFields ) { if (!issueKeys || issueKeys.length === 0) { - console.log("No issue keys provided for update"); - return { successful: 0, failed: 0, errors: [] }; + console.log('No issue keys provided for update') + return { successful: 0, failed: 0, errors: [] } } - console.log(`Updating ${issueKeys.length} issues to status: ${targetStatus}`); + console.log(`Updating ${issueKeys.length} issues to status: ${targetStatus}`) const results = await Promise.allSettled( issueKeys.map((issueKey) => @@ -620,34 +616,34 @@ async function updateIssuesFromCommitHistoryWithCustomFields( customFields ) ) - ); + ) const successful = results.filter( - (result) => result.status === "fulfilled" - ).length; - const failed = results.filter((result) => result.status === "rejected"); + (result) => result.status === 'fulfilled' + ).length + const failed = results.filter((result) => result.status === 'rejected') const errors = failed.map( - (result) => result.reason?.message || "Unknown error" - ); + (result) => result.reason?.message || 'Unknown error' + ) console.log( `Update summary: ${successful} successful, ${failed.length} failed` - ); + ) if (failed.length > 0) { - console.log("Failed updates:", errors); + console.log('Failed updates:', errors) } return { successful, failed: failed.length, errors, - }; + } } /** * Update issues by PR with separate custom field updates */ -async function updateByPRWithCustomFields( +async function updateByPRWithCustomFields ( jiraUtil, prUrl, newStatus, @@ -655,35 +651,35 @@ async function updateByPRWithCustomFields( customFields ) { try { - const jql = `text ~ "${prUrl}"`; - const response = await jiraUtil.request("/search/jql", { - method: "POST", + const jql = `text ~ "${prUrl}"` + const response = await jiraUtil.request('/search/jql', { + method: 'POST', body: JSON.stringify({ jql, - fields: ["key", "summary", "status", "description"], + fields: [ 'key', 'summary', 'status', 'description' ], maxResults: 50, }), - }); + }) - const data = await response.json(); - const issues = data.issues; - console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`); + const data = await response.json() + const issues = data.issues + console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`) for (const issue of issues) { await updateIssueWithCustomFields( jiraUtil, issue.key, newStatus, - ["Blocked", "Rejected"], + [ 'Blocked', 'Rejected' ], transitionFields, customFields - ); + ) } - return issues.length; + return issues.length } catch (error) { - console.error(`Error updating issues by PR:`, error.message); - throw error; + console.error(`Error updating issues by PR:`, error.message) + throw error } } @@ -692,18 +688,18 @@ async function updateByPRWithCustomFields( * @param {Object} pullRequest - GitHub PR object * @returns {Array} Array of Jira issue keys */ -function extractJiraIssueKeys(pullRequest) { - const jiraKeyPattern = /[A-Z]+-[0-9]+/g; - const keys = new Set(); +function extractJiraIssueKeys (pullRequest) { + const jiraKeyPattern = /[A-Z]+-[0-9]+/g + const keys = new Set() if (pullRequest.title) { - const titleMatches = pullRequest.title.match(jiraKeyPattern); + const titleMatches = pullRequest.title.match(jiraKeyPattern) if (titleMatches) { - titleMatches.forEach((key) => keys.add(key)); + titleMatches.forEach((key) => keys.add(key)) } } - return Array.from(keys); + return Array.from(keys) } /** @@ -711,13 +707,13 @@ function extractJiraIssueKeys(pullRequest) { * @param {string} commitMessage - Git commit message * @returns {string|null} PR number or null if not found */ -function extractPrNumber(commitMessage) { - const prMatch = commitMessage.match(/#([0-9]+)/); - return prMatch ? prMatch[1] : null; +function extractPrNumber (commitMessage) { + const prMatch = commitMessage.match(/#([0-9]+)/) + return prMatch ? prMatch[1] : null } // Export helpers for testability module.exports = Object.assign(module.exports || {}, { maskSensitive, detectEnvironment, -}); +}) diff --git a/update_jira/index.test.js b/update_jira/index.test.js index e71e6db..8ab5c34 100644 --- a/update_jira/index.test.js +++ b/update_jira/index.test.js @@ -1,121 +1,121 @@ /** - * Pure Node.js test suite for update_jira/index.js helpers. - * Run: node update_jira/index.test.js + * Unit tests for update_jira/index.js helper functions + * Run with: node update_jira/index.test.js */ -const assert = require("assert"); -const fs = require("fs"); -const { maskSensitive, detectEnvironment } = require("./index"); +const assert = require('node:assert') +const fs = require('node:fs') +const { maskSensitive, detectEnvironment } = require('./index') -function test(title, fn) { +function test (title, fn) { try { - fn(); - console.log(`PASS: ${title}`); + fn() + console.log(`PASS: ${title}`) } catch (e) { - console.error(`FAIL: ${title}\n ${e.stack}`); - process.exitCode = 1; + console.error(`FAIL: ${title}\n ${e.stack}`) + process.exitCode = 1 } } // --- maskSensitive --- -test("maskSensitive masks apiToken, email, headers.Authorization, JIRA_API_TOKEN, JIRA_EMAIL", () => { +test('maskSensitive masks apiToken, email, headers.Authorization, JIRA_API_TOKEN, JIRA_EMAIL', () => { const input = { - apiToken: "secret", - email: "mail", - headers: { Authorization: "tok" }, - JIRA_API_TOKEN: "tok2", - JIRA_EMAIL: "mail2", - other: "ok", - }; - const masked = maskSensitive(input); - assert.strictEqual(masked.apiToken, "***"); - assert.strictEqual(masked.email, "***"); - assert.strictEqual(masked.headers.Authorization, "***"); - assert.strictEqual(masked.JIRA_API_TOKEN, "***"); - assert.strictEqual(masked.JIRA_EMAIL, "***"); - assert.strictEqual(masked.other, "ok"); -}); -test("maskSensitive returns non-object as is", () => { - assert.strictEqual(maskSensitive(null), null); - assert.strictEqual(maskSensitive(123), 123); -}); + apiToken: 'secret', + email: 'mail', + headers: { Authorization: 'tok' }, + JIRA_API_TOKEN: 'tok2', + JIRA_EMAIL: 'mail2', + other: 'ok', + } + const masked = maskSensitive(input) + assert.strictEqual(masked.apiToken, '***') + assert.strictEqual(masked.email, '***') + assert.strictEqual(masked.headers.Authorization, '***') + assert.strictEqual(masked.JIRA_API_TOKEN, '***') + assert.strictEqual(masked.JIRA_EMAIL, '***') + assert.strictEqual(masked.other, 'ok') +}) +test('maskSensitive returns non-object as is', () => { + assert.strictEqual(maskSensitive(null), null) + assert.strictEqual(maskSensitive(123), 123) +}) // --- detectEnvironment --- -test("detectEnvironment detects github", () => { - const old = process.env.GITHUB_ACTIONS; - process.env.GITHUB_ACTIONS = "true"; - assert.strictEqual(detectEnvironment(), "github"); - process.env.GITHUB_ACTIONS = old; -}); -test("detectEnvironment detects ci", () => { +test('detectEnvironment detects github', () => { + const old = process.env.GITHUB_ACTIONS + process.env.GITHUB_ACTIONS = 'true' + assert.strictEqual(detectEnvironment(), 'github') + process.env.GITHUB_ACTIONS = old +}) +test('detectEnvironment detects ci', () => { const old1 = process.env.GITHUB_ACTIONS, - old2 = process.env.CI; - delete process.env.GITHUB_ACTIONS; - process.env.CI = "true"; - assert.strictEqual(detectEnvironment(), "ci"); - process.env.GITHUB_ACTIONS = old1; - process.env.CI = old2; -}); -test("detectEnvironment detects local", () => { + old2 = process.env.CI + delete process.env.GITHUB_ACTIONS + process.env.CI = 'true' + assert.strictEqual(detectEnvironment(), 'ci') + process.env.GITHUB_ACTIONS = old1 + process.env.CI = old2 +}) +test('detectEnvironment detects local', () => { const old1 = process.env.GITHUB_ACTIONS, - old2 = process.env.CI; - delete process.env.GITHUB_ACTIONS; - delete process.env.CI; - assert.strictEqual(detectEnvironment(), "local"); - process.env.GITHUB_ACTIONS = old1; - process.env.CI = old2; -}); + old2 = process.env.CI + delete process.env.GITHUB_ACTIONS + delete process.env.CI + assert.strictEqual(detectEnvironment(), 'local') + process.env.GITHUB_ACTIONS = old1 + process.env.CI = old2 +}) // --- Event payload loading logic (mocked) --- -test("loads event.local.json if present (local env)", () => { +test('loads event.local.json if present (local env)', () => { // Mock fs.existsSync and fs.readFileSync const existsSyncOrig = fs.existsSync, - readFileSyncOrig = fs.readFileSync; - fs.existsSync = (p) => p.includes("event.local.json"); - fs.readFileSync = () => '{"foo":42}'; - let eventData = null; - const localEventPath = "./update_jira/event.local.json"; + readFileSyncOrig = fs.readFileSync + fs.existsSync = (p) => p.includes('event.local.json') + fs.readFileSync = () => '{"foo":42}' + let eventData = null + const localEventPath = './update_jira/event.local.json' if (fs.existsSync(localEventPath)) { - eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + eventData = JSON.parse(fs.readFileSync(localEventPath, 'utf8')) } - assert.deepStrictEqual(eventData, { foo: 42 }); - fs.existsSync = existsSyncOrig; - fs.readFileSync = readFileSyncOrig; -}); -test("loads GITHUB_EVENT_PATH if event.local.json not present (local env)", () => { + assert.deepStrictEqual(eventData, { foo: 42 }) + fs.existsSync = existsSyncOrig + fs.readFileSync = readFileSyncOrig +}) +test('loads GITHUB_EVENT_PATH if event.local.json not present (local env)', () => { const existsSyncOrig = fs.existsSync, - readFileSyncOrig = fs.readFileSync; - process.env.GITHUB_EVENT_PATH = "/fake/path/event.json"; - fs.existsSync = (p) => p === "/fake/path/event.json"; - fs.readFileSync = () => '{"bar":99}'; - let eventData = null; - const localEventPath = "./update_jira/event.local.json"; + readFileSyncOrig = fs.readFileSync + process.env.GITHUB_EVENT_PATH = '/fake/path/event.json' + fs.existsSync = (p) => p === '/fake/path/event.json' + fs.readFileSync = () => '{"bar":99}' + let eventData = null + const localEventPath = './update_jira/event.local.json' if (fs.existsSync(localEventPath)) { - eventData = JSON.parse(fs.readFileSync(localEventPath, "utf8")); + eventData = JSON.parse(fs.readFileSync(localEventPath, 'utf8')) } else if ( process.env.GITHUB_EVENT_PATH && fs.existsSync(process.env.GITHUB_EVENT_PATH) ) { eventData = JSON.parse( - fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8") - ); + fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8') + ) } - assert.deepStrictEqual(eventData, { bar: 99 }); - fs.existsSync = existsSyncOrig; - fs.readFileSync = readFileSyncOrig; -}); + assert.deepStrictEqual(eventData, { bar: 99 }) + fs.existsSync = existsSyncOrig + fs.readFileSync = readFileSyncOrig +}) // --- Dry-run mode logic --- -test("dry-run mode skips update logic", () => { - process.env.DRY_RUN = "true"; - let called = false; - function mockJiraUpdate() { - called = true; +test('dry-run mode skips update logic', () => { + process.env.DRY_RUN = 'true' + let called = false + function mockJiraUpdate () { + called = true } - if (process.env.DRY_RUN === "true") { + if (process.env.DRY_RUN === 'true') { // Should not call mockJiraUpdate } else { - mockJiraUpdate(); + mockJiraUpdate() } - assert.strictEqual(called, false); - delete process.env.DRY_RUN; -}); + assert.strictEqual(called, false) + delete process.env.DRY_RUN +}) diff --git a/utils/jira.integration.test.js b/utils/jira.integration.test.js index e83b063..7099f77 100644 --- a/utils/jira.integration.test.js +++ b/utils/jira.integration.test.js @@ -12,33 +12,31 @@ * At the end, you can revert all changes made by this test by answering 'yes' to the prompt. */ -require("dotenv").config(); -const Jira = require("./jira"); +require('dotenv').config() +const Jira = require('./jira') /** * Mask sensitive data in logs. * @param {object} obj * @returns {object} */ -function maskSensitive(obj) { - const clone = - typeof structuredClone === "function" - ? structuredClone(obj) - : JSON.parse(JSON.stringify(obj)); - if (clone.apiToken) clone.apiToken = "***"; - if (clone.email) clone.email = "***"; - if (clone.headers?.Authorization) clone.headers.Authorization = "***"; - return clone; +function maskSensitive (obj) { + if (!obj || typeof obj !== 'object') return obj + const clone = structuredClone(obj) + if (clone.apiToken) clone.apiToken = '***' + if (clone.email) clone.email = '***' + if (clone.headers?.Authorization) clone.headers.Authorization = '***' + return clone } /** * Log a section header for test output. * @param {string} title */ -function logSection(title) { - console.log("\n===================="); - console.log(title); - console.log("===================="); +function logSection (title) { + console.log('\n====================') + console.log(title) + console.log('====================') } /** @@ -48,21 +46,21 @@ function logSection(title) { * @param {string} customField * @returns {Promise<{status: string, customFieldValue: any}>} */ -async function captureOriginalIssueState(jira, issueKey, customField) { - logSection("Capture original issue state for rollback"); +async function captureOriginalIssueState (jira, issueKey, customField) { + logSection('Capture original issue state for rollback') try { const issueResp = await jira.request( `/issue/${issueKey}?fields=status,${customField}` - ); - const issueData = await issueResp.json(); - const status = issueData.fields.status.name; - const customFieldValue = issueData.fields[customField]; - console.log(`Original status: ${status}`); - console.log(`Original custom field (${customField}):`, customFieldValue); - return { status, customFieldValue }; + ) + const issueData = await issueResp.json() + const status = issueData.fields.status.name + const customFieldValue = issueData.fields[customField] + console.log(`Original status: ${status}`) + console.log(`Original custom field (${customField}):`, customFieldValue) + return { status, customFieldValue } } catch (err) { - console.error("Failed to capture original state:", err.message); - return { status: null, customFieldValue: null }; + console.error('Failed to capture original state:', err.message) + return { status: null, customFieldValue: null } } } @@ -75,47 +73,47 @@ async function captureOriginalIssueState(jira, issueKey, customField) { * @param {string} originalStatus * @returns {Promise} */ -async function rollbackIssueState( +async function rollbackIssueState ( jira, issueKey, customField, originalCustomFieldValue, originalStatus ) { - logSection("ROLLBACK: Reverting all changes made by this test..."); - let rollbackErrors = false; + logSection('ROLLBACK: Reverting all changes made by this test...') + let rollbackErrors = false try { await jira.updateCustomField( issueKey, customField, originalCustomFieldValue - ); + ) console.log( `Rolled back custom field ${customField} to:`, originalCustomFieldValue - ); + ) } catch (err) { - console.error("Failed to rollback custom field:", err.message); - rollbackErrors = true; + console.error('Failed to rollback custom field:', err.message) + rollbackErrors = true } try { - const issueResp = await jira.request(`/issue/${issueKey}?fields=status`); - const issueData = await issueResp.json(); - const currentStatus = issueData.fields.status.name; + const issueResp = await jira.request(`/issue/${issueKey}?fields=status`) + const issueData = await issueResp.json() + const currentStatus = issueData.fields.status.name if (originalStatus && currentStatus !== originalStatus) { - await jira.transitionIssue(issueKey, originalStatus); - console.log(`Rolled back status to: ${originalStatus}`); + await jira.transitionIssue(issueKey, originalStatus) + console.log(`Rolled back status to: ${originalStatus}`) } else { - console.log("No status rollback needed."); + console.log('No status rollback needed.') } } catch (err) { - console.error("Failed to rollback status:", err.message); - rollbackErrors = true; + console.error('Failed to rollback status:', err.message) + rollbackErrors = true } if (rollbackErrors) { - console.log("Rollback completed with errors. Check logs above."); + console.log('Rollback completed with errors. Check logs above.') } else { - console.log("Rollback completed successfully."); + console.log('Rollback completed successfully.') } } @@ -131,232 +129,232 @@ async function rollbackIssueState( baseUrl: process.env.JIRA_BASE_URL, email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN, - }); + }) - logSection("Jira instance created"); - console.dir(maskSensitive(jira), { depth: 1 }); + logSection('Jira instance created') + console.dir(maskSensitive(jira), { depth: 1 }) // Test configuration - const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || "DEX-36"; - const testProjectKey = process.env.TEST_JIRA_PROJECT_KEY || "DEX"; + const testIssueKey = process.env.TEST_JIRA_ISSUE_KEY || 'DEX-36' + const testProjectKey = process.env.TEST_JIRA_PROJECT_KEY || 'DEX' const testCustomField = - process.env.TEST_JIRA_CUSTOM_FIELD || "customfield_10001"; - const testCustomValue = process.env.TEST_JIRA_CUSTOM_VALUE || "test-value"; - const testStatus = process.env.TEST_JIRA_STATUS || "Done"; + process.env.TEST_JIRA_CUSTOM_FIELD || 'customfield_10001' + const testCustomValue = process.env.TEST_JIRA_CUSTOM_VALUE || 'test-value' + const testStatus = process.env.TEST_JIRA_STATUS || 'Done' const testPRUrl = process.env.TEST_JIRA_PR_URL || - "https://github.com/coursedog/notion-scripts/pull/42"; + 'https://github.com/coursedog/notion-scripts/pull/42' // --- CAPTURE ORIGINAL STATE --- const { status: originalStatus, customFieldValue: originalCustomFieldValue } = - await captureOriginalIssueState(jira, testIssueKey, testCustomField); + await captureOriginalIssueState(jira, testIssueKey, testCustomField) // --- TEST CASES --- try { - logSection("Test: List all workflows"); - const workflows = await jira.getAllWorkflows(); + logSection('Test: List all workflows') + const workflows = await jira.getAllWorkflows() console.log( - "Workflows:", + 'Workflows:', workflows.map((w) => w.name || w.id) - ); + ) } catch (err) { - console.error("getAllWorkflows error:", err.message); + console.error('getAllWorkflows error:', err.message) } try { - logSection("Test: Get project workflow name"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - console.log("Workflow name for project", testProjectKey, ":", wfName); + logSection('Test: Get project workflow name') + const wfName = await jira.getProjectWorkflowName(testProjectKey) + console.log('Workflow name for project', testProjectKey, ':', wfName) } catch (err) { - console.error("getProjectWorkflowName error:", err.message); + console.error('getProjectWorkflowName error:', err.message) } try { - logSection("Test: Get workflow state machine"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - const sm = await jira.getWorkflowStateMachine(wfName); - console.log("State machine states:", Object.keys(sm.states)); + logSection('Test: Get workflow state machine') + const wfName = await jira.getProjectWorkflowName(testProjectKey) + const sm = await jira.getWorkflowStateMachine(wfName) + console.log('State machine states:', Object.keys(sm.states)) } catch (err) { - console.error("getWorkflowStateMachine error:", err.message); + console.error('getWorkflowStateMachine error:', err.message) } try { - logSection("Test: Get available transitions for issue"); - const transitions = await jira.getTransitions(testIssueKey); + logSection('Test: Get available transitions for issue') + const transitions = await jira.getTransitions(testIssueKey) console.log( - "Transitions:", + 'Transitions:', transitions.map((t) => `${t.name} → ${t.to.name}`) - ); + ) } catch (err) { - console.error("getTransitions error:", err.message); + console.error('getTransitions error:', err.message) } try { - logSection("Test: Find issues by status"); - const issues = await jira.findByStatus(testStatus); + logSection('Test: Find issues by status') + const issues = await jira.findByStatus(testStatus) console.log( - "Issues in status", + 'Issues in status', testStatus, - ":", + ':', issues.map((i) => i.key) - ); + ) } catch (err) { - console.error("findByStatus error:", err.message); + console.error('findByStatus error:', err.message) } try { - logSection("Test: List all statuses"); - const statuses = await jira.getAllStatuses(); + logSection('Test: List all statuses') + const statuses = await jira.getAllStatuses() console.log( - "Statuses:", + 'Statuses:', statuses.map((s) => s.name) - ); + ) } catch (err) { - console.error("getAllStatuses error:", err.message); + console.error('getAllStatuses error:', err.message) } try { - logSection('Test: Get field options for "resolution"'); - const options = await jira.getFieldOptions("resolution"); + logSection('Test: Get field options for "resolution"') + const options = await jira.getFieldOptions('resolution') console.log( - "Resolution options:", + 'Resolution options:', options.map((o) => o.name) - ); + ) } catch (err) { - console.error("getFieldOptions error:", err.message); + console.error('getFieldOptions error:', err.message) } try { - logSection("Test: Get workflow schema"); - const schema = await jira.getWorkflowSchema(testProjectKey); - console.log("Workflow schema:", schema); + logSection('Test: Get workflow schema') + const schema = await jira.getWorkflowSchema(testProjectKey) + console.log('Workflow schema:', schema) } catch (err) { - console.error("getWorkflowSchema error:", err.message); + console.error('getWorkflowSchema error:', err.message) } try { - logSection("Test: Update custom field (may fail if value is invalid)"); + logSection('Test: Update custom field (may fail if value is invalid)') const res = await jira.updateCustomField( testIssueKey, testCustomField, testCustomValue - ); - console.log("updateCustomField result:", res); + ) + console.log('updateCustomField result:', res) } catch (err) { - console.error("updateCustomField error:", err.message); + console.error('updateCustomField error:', err.message) } try { - logSection("Test: Get custom field value"); - const val = await jira.getCustomField(testIssueKey, testCustomField); - console.log("Custom field value:", val); + logSection('Test: Get custom field value') + const val = await jira.getCustomField(testIssueKey, testCustomField) + console.log('Custom field value:', val) } catch (err) { - console.error("getCustomField error:", err.message); + console.error('getCustomField error:', err.message) } try { logSection( - "Test: Update multiple custom fields (may fail if value is invalid)" - ); + 'Test: Update multiple custom fields (may fail if value is invalid)' + ) const res = await jira.updateCustomFields(testIssueKey, { [testCustomField]: testCustomValue, - }); - console.log("updateCustomFields result:", res); + }) + console.log('updateCustomFields result:', res) } catch (err) { - console.error("updateCustomFields error:", err.message); + console.error('updateCustomFields error:', err.message) } try { - logSection("Test: Get transition details for first available transition"); - const transitions = await jira.getTransitions(testIssueKey); + logSection('Test: Get transition details for first available transition') + const transitions = await jira.getTransitions(testIssueKey) if (transitions && transitions.length > 0) { const details = await jira.getTransitionDetails( testIssueKey, transitions[0].id - ); - console.log("Transition details:", details); + ) + console.log('Transition details:', details) } else { - console.log("No transitions to get details for"); + console.log('No transitions to get details for') } } catch (err) { - console.error("getTransitionDetails error:", err.message); + console.error('getTransitionDetails error:', err.message) } try { - logSection("Test: Extract issue keys from commit messages"); + logSection('Test: Extract issue keys from commit messages') const keys = jira.extractIssueKeysFromCommitMessages([ - "DEX-36: test commit", - "ALL-123: another commit", - "no key here", - ]); - console.log("Extracted keys:", keys); + 'DEX-36: test commit', + 'ALL-123: another commit', + 'no key here', + ]) + console.log('Extracted keys:', keys) } catch (err) { - console.error("extractIssueKeysFromCommitMessages error:", err.message); + console.error('extractIssueKeysFromCommitMessages error:', err.message) } try { - logSection("Test: Find all/shortest transition paths"); - const wfName = await jira.getProjectWorkflowName(testProjectKey); - const sm = await jira.getWorkflowStateMachine(wfName); - const allPaths = jira.findAllTransitionPaths(sm, "To Do", testStatus); - const shortest = jira.findShortestTransitionPath(sm, "To Do", testStatus); - console.log("All paths count:", allPaths.length); - console.log("Shortest path:", shortest); + logSection('Test: Find all/shortest transition paths') + const wfName = await jira.getProjectWorkflowName(testProjectKey) + const sm = await jira.getWorkflowStateMachine(wfName) + const allPaths = jira.findAllTransitionPaths(sm, 'To Do', testStatus) + const shortest = jira.findShortestTransitionPath(sm, 'To Do', testStatus) + console.log('All paths count:', allPaths.length) + console.log('Shortest path:', shortest) } catch (err) { console.error( - "findShortestTransitionPath/findAllTransitionPaths error:", + 'findShortestTransitionPath/findAllTransitionPaths error:', err.message - ); + ) } try { logSection( - "Test: Update issues by status (may fail if workflow transition not allowed)" - ); - await jira.updateByStatus(testStatus, "In Progress", {}); + 'Test: Update issues by status (may fail if workflow transition not allowed)' + ) + await jira.updateByStatus(testStatus, 'In Progress', {}) } catch (err) { - console.error("updateByStatus error:", err.message); + console.error('updateByStatus error:', err.message) } try { - logSection("Test: Update issues by PR URL"); - await jira.updateByPR(testPRUrl, testStatus, {}); + logSection('Test: Update issues by PR URL') + await jira.updateByPR(testPRUrl, testStatus, {}) } catch (err) { - console.error("updateByPR error:", err.message); + console.error('updateByPR error:', err.message) } try { - logSection("Test: Update issues from commit history"); - await jira.updateIssuesFromCommitHistory(["DEX-36", "ALL-123"], testStatus); + logSection('Test: Update issues from commit history') + await jira.updateIssuesFromCommitHistory([ 'DEX-36', 'ALL-123' ], testStatus) } catch (err) { - console.error("updateIssuesFromCommitHistory error:", err.message); + console.error('updateIssuesFromCommitHistory error:', err.message) } try { - logSection("Test: Transition issue to target status"); - await jira.transitionIssue(testIssueKey, testStatus); + logSection('Test: Transition issue to target status') + await jira.transitionIssue(testIssueKey, testStatus) } catch (err) { - console.error("transitionIssue error:", err.message); + console.error('transitionIssue error:', err.message) } // --- END OF TEST CASES --- - logSection("TEST COMPLETE"); - console.log("Do you want to revert all changes made by this test? (yes/no)"); - process.stdin.setEncoding("utf8"); - process.stdin.once("data", async (data) => { - if (data.trim().toLowerCase() === "yes") { + logSection('TEST COMPLETE') + console.log('Do you want to revert all changes made by this test? (yes/no)') + process.stdin.setEncoding('utf8') + process.stdin.once('data', async (data) => { + if (data.trim().toLowerCase() === 'yes') { await rollbackIssueState( jira, testIssueKey, testCustomField, originalCustomFieldValue, originalStatus - ); - process.exit(0); + ) + process.exit(0) } else { - console.log("No revert performed. All changes made by this test remain."); - process.exit(0); + console.log('No revert performed. All changes made by this test remain.') + process.exit(0) } - }); -})(); + }) +})() diff --git a/utils/jira.js b/utils/jira.js index 809ccd5..dc25ae6 100644 --- a/utils/jira.js +++ b/utils/jira.js @@ -734,7 +734,7 @@ class Jira { * @throws {Error} If git command fails unexpectedly (not due to empty range or missing refs) */ async getIssueKeysFromCommitHistory (fromRef, toRef) { - const { execSync } = require('child_process') + const { execSync } = require('node:child_process') // Validate input parameters if (