diff --git a/.github/CODEOWNERS.md b/.github/CODEOWNERS.md
new file mode 100644
index 00000000..817d38d8
--- /dev/null
+++ b/.github/CODEOWNERS.md
@@ -0,0 +1,10 @@
+# Code Owners
+
+
+
+[@sachin-panayil](https://github.com/sachin-panayil)
+[@natalialuzuriaga](https://github.com/natalialuzuriaga)
+
+## Repository Domains
+
+/src/types - [@natalialuzuriaga](https://github.com/natalialuzuriaga)
\ No newline at end of file
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..a34219ed
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,29 @@
+name: Lint
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ - dev
+
+jobs:
+ eslint:
+ name: ESLint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run ESLint
+ run: npm run lint
diff --git a/.github/workflows/release-warning.yml b/.github/workflows/release-warning.yml
new file mode 100644
index 00000000..89b4bed5
--- /dev/null
+++ b/.github/workflows/release-warning.yml
@@ -0,0 +1,50 @@
+name: Require release label for main PRs
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+ branches:
+ - main
+
+permissions:
+ pull-requests: read
+
+jobs:
+ require-release-label:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check for release label
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const labels = context.payload.pull_request.labels.map(l => l.name);
+
+ const allowed = [
+ 'release:patch',
+ 'release:minor',
+ 'release:major',
+ 'release:none'
+ ];
+
+ const found = labels.filter(l => allowed.includes(l));
+
+ if (found.length === 0) {
+ core.setFailed(
+ "Missing release label. Add one of: " +
+ "release:patch | release:minor | release:major | release:none"
+ );
+ }
+
+ if (found.length > 1) {
+ core.setFailed(
+ "Multiple release labels found. Exactly one is required."
+ );
+ }
+
+ console.log(`Release label OK: ${found[0]}`);
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..1cc18880
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,98 @@
+name: Release a New Verison
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: read
+
+jobs:
+ release:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get release label
+ id: label
+ run: |
+ labels='${{ toJson(github.event.pull_request.labels) }}'
+
+ if echo "$labels" | grep -q 'release:major'; then
+ echo "bump=major" >> $GITHUB_OUTPUT
+ elif echo "$labels" | grep -q 'release:minor'; then
+ echo "bump=minor" >> $GITHUB_OUTPUT
+ elif echo "$labels" | grep -q 'release:patch'; then
+ echo "bump=patch" >> $GITHUB_OUTPUT
+ elif echo "$labels" | grep -q 'release:none'; then
+ echo "bump=none" >> $GITHUB_OUTPUT
+ else
+ echo "No release label found. Add one of:"
+ echo "release:patch | release:minor | release:major | release:none"
+ exit 1
+ fi
+
+ - name: Exit if no release needed
+ if: steps.label.outputs.bump == 'none'
+ run: |
+ echo "No release requested. Exiting."
+ exit 0
+
+ - name: Get latest version tag
+ id: version
+ run: |
+ latest=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
+ echo "latest=$latest" >> $GITHUB_OUTPUT
+
+ - name: Calculate next version
+ id: next
+ run: |
+ version=${{ steps.version.outputs.latest }}
+ bump=${{ steps.label.outputs.bump }}
+
+ version=${version#v}
+ IFS='.' read -r major minor patch <<< "$version"
+
+ case "$bump" in
+ major)
+ major=$((major+1))
+ minor=0
+ patch=0
+ ;;
+ minor)
+ minor=$((minor+1))
+ patch=0
+ ;;
+ patch)
+ patch=$((patch+1))
+ ;;
+ esac
+
+ next="v$major.$minor.$patch"
+ echo "next=$next" >> $GITHUB_OUTPUT
+
+ - name: Create version tag
+ run: |
+ git tag ${{ steps.next.outputs.next }}
+ git push origin ${{ steps.next.outputs.next }}
+
+ - name: Update moving major tag (v1)
+ run: |
+ major=$(echo "${{ steps.next.outputs.next }}" | cut -d. -f1)
+ git tag -f $major
+ git push origin $major --force
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: ${{ steps.next.outputs.next }}
+ name: Release ${{ steps.next.outputs.next }}
+ generate_release_notes: true
diff --git a/CODEOWNERS.md b/CODEOWNERS.md
deleted file mode 100644
index bc40d1cc..00000000
--- a/CODEOWNERS.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Code Owners
-
-
-
-- [@sachin-panayil](https://github.com/sachin-panayil)
-
-## Repository Domains
-
-
-
-- Github Action Script in /src/ - [@sachin-panayil](https://github.com/sachin-panayil)
diff --git a/COMMUNITY.md b/COMMUNITY.md
new file mode 100644
index 00000000..e6c0aaf7
--- /dev/null
+++ b/COMMUNITY.md
@@ -0,0 +1,144 @@
+# COMMUNITY.md
+
+automated-codejson-generator is supported by a dedicated team of individuals fulfilling various roles to ensure its success, security, and alignment with government standards and agency goals.
+
+## Project Members
+
+
+
+| Role | Name | Affiliation |
+| :---------------- | :------------------ | :---------- |
+| Open Source Lead | Remy DeCausemaker | DSAC |
+| Software Engineer | Sachin Panayil | DSAC |
+| Software Engineer | Natalia Luzuriaga | DSAC |
+| Software Engineer | Isaac Milarsky | DSAC |
+| Software Engineer | Dinne Kopelevich | DSAC |
+
+See [CODEOWNERS.md](.github/CODEOWNERS.md) for a list of those responsible for the code and documentation in this repository.
+
+See [Community Guidelines](COMMUNITY.md) on principles and guidelines for participating in this open source project.
+
+## Roles & Responsibilities
+
+The members of automated-codejson-generator community are responsible for guiding its development, ensuring quality standards, and fostering a collaborative environment. They play a vital role in making decisions about code contributions, handling releases, and ensuring the project meets its goals and objectives. Below is a list of the key members and their specific roles and responsibilities. We are eagerly seeking individuals who are interested in joining the community and helping shape and support these roles.
+
+### Maintainers:
+
+GitHub Action
+
+- [@sachin-panayil](https://github.com/sachin-panayil)
+
+Scripting
+
+- [@sachin-panayil](https://github.com/sachin-panayil)
+
+### Approvers:
+
+- [@decause-gov](https://github.com/decause-gov)
+
+### Reviewers:
+
+- [@natalialuzuriaga](https://github.com/natalialuzuriaga)
+- [@IsaacMilarky](https://github.com/IsaacMilarky)
+- [@sachin-panayil](https://github.com/sachin-panayil)
+- [@DinneK](https://github.com/DinneK)
+
+| Roles | Responsibilities | Requirements | Defined by |
+| ---------- | :--------------------------------------------- | :-------------------------------------------------------------------------------- | :-------------------------------------------------------- |
+| member | active contributor in the community | multiple contributions to the project. | PROJECT GitHub org Committer Team |
+| reviewer | review contributions from other members | history of review and authorship in a sub-project | COMMUNITY file reviewer entry, and GitHub Org Triage Team |
+| approver | approve accepting contributions | highly experienced and active reviewer + contributor to a sub-project | COMMUNITY file approver entry and GitHub Triage Team |
+| maintainer | set direction and priorities for a sub-project | demonstrated responsibility and excellent technical judgement for the sub-project | COMMUNITY file owner entry and GitHub Org Admin Team |
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on the release process.
+
+## Contributors
+
+
+
+)
+
+
+
+
+
+
+
+## automated-codejson-generator Open Source Community Guidelines
+
+This document contains principles and guidelines for participating in the automated-codejson-generator open source community.
+
+### Principles
+
+These principles guide our data, product, and process decisions, architecture, and approach.
+
+- Open means transparent and participatory.
+- We take a modular and modern approach to software development.
+- We build open-source software and open-source process.
+- We value ease of implementation.
+- Fostering community includes building capacity and making our software and processes accessible to participants with diverse backgrounds and skillsets.
+- Data (and data science) is as important as software and process. We build open data sets where possible.
+- We strive for transparency for algorithms and places we might be introducing bias.
+
+### Community Guidelines
+
+All community members are expected to adhere to our [Code of Conduct](CODE_OF_CONDUCT.md).
+
+Information on contributing to this repository is available in our [Contributing file](CONTRIBUTING.md).
+
+When participating in Code.json Auto Generator open source community conversations and spaces, we ask individuals to follow the following guidelines:
+
+- When joining a conversation for the first time, please introduce yourself by providing a brief intro that includes:
+ - your related organization (if applicable)
+ - your pronouns
+ - your superpower, and how you hope to use it for automated-codejson-generator
+- Embrace a culture of learning, and educate each other. We are all entering this conversation from different starting points and with different backgrounds. There are no dumb questions.
+- Take space and give space. We strive to create an equitable environment in which all are welcome and able to participate. We hope individuals feel comfortable voicing their opinions and providing contributions and will do our best to recognize and make space for individuals who may be struggling to find space here. Likewise, we expect individuals to recognize when they are taking up significant space and take a step back to allow room for others.
+
+- Be respectful.
+- Default to positive. Assume others' contributions are legitimate and valuable and that they are made with good intention.
+
+### Acknowledgements
+
+The Community Guidelines sections were originally forked from the [United States Digital Service](https://usds.gov) [Justice40](https://thejustice40.com) open source [repository](https://github.com/usds/justice40-tool), and we would like to acknowledge and thank the community for their contributions.
\ No newline at end of file
diff --git a/COMMUNITY_GUIDELINES.md b/COMMUNITY_GUIDELINES.md
deleted file mode 100644
index f4f010f0..00000000
--- a/COMMUNITY_GUIDELINES.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# {name_of_project_here} Open Source Community Guidelines
-
-This document contains principles and guidelines for participating in the {name_of_project_here} open source community.
-
-## Principles
-
-These principles guide our data, product, and process decisions, architecture, and approach.
-
-- Open means transparent and participatory.
-- We take a modular and modern approach to software development.
-- We build open-source software and open-source process.
-- We value ease of implementation.
-- Fostering community includes building capacity and making our software and processes accessible to participants with diverse backgrounds and skillsets.
-- Data (and data science) is as important as software and process. We build open data sets where possible.
-- We strive for transparency for algorithms and places we might be introducing bias.
-
-## Community Guidelines
-
-All community members are expected to adhere to our [Code of Conduct](CODE_OF_CONDUCT.md).
-Information on contributing to this repository is available in our [Contributing file](CONTRIBUTING.md).
-When participating in automated-codejson-generator open source community conversations and spaces, we ask individuals to follow the following guidelines:
-
-- When joining a conversation for the first time, please introduce yourself by providing a brief intro that includes:
-- your related organization (if applicable)
-- your pronouns
-- your superpower, and how you hope to use it for automated-codejson-generator
-- Embrace a culture of learning, and educate each other. We are all entering this conversation from different starting points and with different backgrounds. There are no dumb questions.
-- Take space and give space. We strive to create an equitable environment in which all are welcome and able to participate. We hope individuals feel comfortable voicing their opinions and providing contributions and will do our best to recognize and make space for individuals who may be struggling to find space here. Likewise, we expect individuals to recognize when they are taking up significant space and take a step back to allow room for others.
-- Be respectful.
-- Default to positive. Assume others' contributions are legitimate and valuable and that they are made with good intention.
-
-## Acknowledgements
-
-This COMMUNITY_GUIDELINES.md was originally forked from the [United States Digital Service](https://usds.gov) [Justice40](https://thejustice40.com) open source [repository](https://github.com/usds/justice40-tool), and we would like to acknowledge and thank the community for their contributions.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4d68bed7..23f9a906 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ Look for issues labeled `good-first-issue` for good opportunities to contribute.
### Team Specific Guidelines
-Our project maintainers are listed in [MAINTAINERS.md](MAINTAINERS.md). They are responsible for reviewing and merging all pull requests. Feel free to tag them in issues or pull requests for assistance.
+Our project maintainers are listed in [COMMUNITY.md](COMMUNITY.md). They are responsible for reviewing and merging all pull requests. Feel free to tag them in issues or pull requests for assistance.
### Building Dependencies
@@ -25,14 +25,14 @@ To work on this project, you'll need:
```bash
# Clone the repository
-git clone https://github.com/DSACMS/code-json-generator.git
-cd code-json-generator
+git clone https://github.com/DSACMS/automated-codejson-generator.git
+cd automated-codejson-generator
# Install dependencies
npm install
# Build the project
-npm run package
+npm run bundle
# Run tests
npm test
@@ -52,16 +52,23 @@ When the `pull_request` trigger is configured, the action validates code.json wh
### Workflow and Branching
-We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/):
+We follow a **GitHub Flow–inspired workflow** with a protected `main` branch and a `dev` integration branch.
1. Fork the project
-2. Check out the `main` branch
-3. Create a feature branch
+2. Check out the `dev` branch
+3. Create a feature branch from `dev`
4. Write code and tests for your change
-5. From your branch, make a pull request against `DSACMS/code-json-generator/main`
-6. Work with repo maintainers to get your change reviewed
-7. Wait for your change to be pulled into `DSACMS/code-json-generator/main`
-8. Delete your feature branch
+5. Open a pull request from your feature branch **into `dev`**
+6. Work with repo maintainers to get your change reviewed and merged into `dev`
+7. When `dev` is ready for release, open a pull request from **`dev` into `main`**
+8. Add **exactly one** release label to the PR:
+ - `release:patch`
+ - `release:minor`
+ - `release:major`
+9. Once the required checks pass, merge the PR into `main`
+ - This triggers automatic versioning, tagging, and GitHub Release creation
+10. Delete your feature branch after merge
+
### Testing Conventions
@@ -123,7 +130,7 @@ feat(scope): description of feature
### Reviewing Pull Requests
-Pull requests are reviewed by the maintainers listed in [MAINTAINERS.md](MAINTAINERS.md). Reviews will check for:
+Pull requests are reviewed by the maintainers listed in [COMMUNITY.md](COMMUNITY.md). Reviews will check for:
- Code quality and style
- Test coverage
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 00000000..196d3f8e
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,9 @@
+import tseslint from "typescript-eslint";
+
+export default [
+ {
+ ignores: ["dist/**", "node_modules/**", "src/__tests__"],
+ },
+
+ ...tseslint.configs.recommended,
+];
diff --git a/package-lock.json b/package-lock.json
index 48620b0d..0fdbb0cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,6 @@
"zod-validation-error": "^5.0.0"
},
"devDependencies": {
- "@eslint/compat": "^1.2.6",
"@github/local-action": "^2.6.1",
"@jest/globals": "^29.7.0",
"@rollup/plugin-commonjs": "^28.0.1",
@@ -27,7 +26,7 @@
"@types/node": "^20.17.17",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
- "eslint": "^9.19.0",
+ "eslint": "^9.39.2",
"eslint-config-prettier": "^10.0.1",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
@@ -41,7 +40,8 @@
"ts-jest": "^29.2.5",
"ts-jest-resolver": "^2.0.1",
"ts-node": "^10.9.2",
- "typescript": "^5.7.3"
+ "typescript": "^5.7.3",
+ "typescript-eslint": "^8.54.0"
},
"engines": {
"node": ">=20"
@@ -1270,56 +1270,42 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "eslint-visitor-keys": "^3.3.0"
+ "eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
- "node_modules/@eslint/compat": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.6.tgz",
- "integrity": "sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "peerDependencies": {
- "eslint": "^9.10.0"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
"node_modules/@eslint/config-array": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
- "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.5",
+ "@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -1328,9 +1314,9 @@
}
},
"node_modules/@eslint/config-array/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==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1351,11 +1337,25 @@
"node": "*"
}
},
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/core": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
- "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@@ -1364,9 +1364,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
- "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1376,7 +1376,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
+ "js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -1388,9 +1388,9 @@
}
},
"node_modules/@eslint/eslintrc/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==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1399,9 +1399,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1412,15 +1412,15 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.14.0",
+ "acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1443,19 +1443,22 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.19.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
- "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
- "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1463,12 +1466,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
- "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.10.0",
+ "@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
@@ -1627,9 +1631,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@humanwhocodes/retry": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
- "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3153,7 +3157,8 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -3201,21 +3206,20 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
- "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
+ "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.23.0",
- "@typescript-eslint/type-utils": "8.23.0",
- "@typescript-eslint/utils": "8.23.0",
- "@typescript-eslint/visitor-keys": "8.23.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/type-utils": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.0.1"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3225,15 +3229,25 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "@typescript-eslint/parser": "^8.54.0",
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
- "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3244,17 +3258,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
- "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
+ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.23.0",
- "@typescript-eslint/types": "8.23.0",
- "@typescript-eslint/typescript-estree": "8.23.0",
- "@typescript-eslint/visitor-keys": "8.23.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3265,18 +3279,40 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
+ "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.54.0",
+ "@typescript-eslint/types": "^8.54.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
- "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
+ "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.23.0",
- "@typescript-eslint/visitor-keys": "8.23.0"
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3286,17 +3322,35 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
+ "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
- "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
+ "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.23.0",
- "@typescript-eslint/utils": "8.23.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.0.1"
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3307,13 +3361,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
- "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3324,9 +3378,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
- "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
+ "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3338,20 +3392,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
- "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
+ "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.23.0",
- "@typescript-eslint/visitor-keys": "8.23.0",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.0.1"
+ "@typescript-eslint/project-service": "8.54.0",
+ "@typescript-eslint/tsconfig-utils": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3361,7 +3416,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <5.8.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@@ -3381,9 +3436,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
- "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3394,16 +3449,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
- "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
+ "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.23.0",
- "@typescript-eslint/types": "8.23.0",
- "@typescript-eslint/typescript-estree": "8.23.0"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3414,18 +3469,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
- "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
+ "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.23.0",
- "eslint-visitor-keys": "^4.2.0"
+ "@typescript-eslint/types": "8.54.0",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3436,9 +3491,9 @@
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3468,9 +3523,9 @@
}
},
"node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4514,12 +4569,13 @@
}
},
"node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -4981,32 +5037,32 @@
}
},
"node_modules/eslint": {
- "version": "9.19.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
- "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.19.0",
- "@eslint/core": "^0.10.0",
- "@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "9.19.0",
- "@eslint/plugin-kit": "^0.2.5",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.1",
+ "@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.2.0",
- "eslint-visitor-keys": "^4.2.0",
- "espree": "^10.3.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -5111,31 +5167,6 @@
}
}
},
- "node_modules/eslint-import-resolver-typescript/node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-import-resolver-typescript/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/eslint-module-utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
@@ -5390,9 +5421,9 @@
}
},
"node_modules/eslint/node_modules/eslint-scope": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
- "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -5407,9 +5438,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -5420,15 +5451,15 @@
}
},
"node_modules/eslint/node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.14.0",
+ "acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7554,9 +7585,9 @@
"dev": true
},
"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==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8022,10 +8053,11 @@
}
},
"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
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
@@ -9344,10 +9376,11 @@
}
},
"node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -9857,6 +9890,54 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -10256,6 +10337,30 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.54.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
+ "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.54.0",
+ "@typescript-eslint/parser": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
diff --git a/package.json b/package.json
index cbc5ebc8..2603c012 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,6 @@
"zod-validation-error": "^5.0.0"
},
"devDependencies": {
- "@eslint/compat": "^1.2.6",
"@github/local-action": "^2.6.1",
"@jest/globals": "^29.7.0",
"@rollup/plugin-commonjs": "^28.0.1",
@@ -56,7 +55,7 @@
"@types/node": "^20.17.17",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
- "eslint": "^9.19.0",
+ "eslint": "^9.39.2",
"eslint-config-prettier": "^10.0.1",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
@@ -70,7 +69,8 @@
"ts-jest": "^29.2.5",
"ts-jest-resolver": "^2.0.1",
"ts-node": "^10.9.2",
- "typescript": "^5.7.3"
+ "typescript": "^5.7.3",
+ "typescript-eslint": "^8.54.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
diff --git a/src/__tests__/fixtures/mock-deps.ts b/src/__tests__/fixtures/mock-deps.ts
new file mode 100644
index 00000000..316622a9
--- /dev/null
+++ b/src/__tests__/fixtures/mock-deps.ts
@@ -0,0 +1,87 @@
+import { jest } from "@jest/globals";
+import { Dependencies, OctokitClient, Logger } from "../../types/Dependencies.js";
+
+
+export function createMockLogger(): Logger & { [K in keyof Logger]: jest.Mock } {
+ return {
+ info: jest.fn(),
+ error: jest.fn(),
+ warning: jest.fn(),
+ debug: jest.fn(),
+ };
+ }
+
+// creates a mock OctokitClient with sensible defaults
+export function createMockOctokit(overrides: Partial> = {}): OctokitClient {
+ return {
+ rest: {
+ repos: {
+ get: overrides.rest?.repos?.get as OctokitClient["rest"]["repos"]["get"] ??
+ jest.fn().mockResolvedValue({
+ data: {
+ name: "test-repo",
+ description: "A test repository",
+ html_url: "https://github.com/test-owner/test-repo",
+ private: false,
+ forks_count: 5,
+ topics: ["test", "automation"],
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-06-01T00:00:00Z",
+ default_branch: "main",
+ },
+ }),
+ listLanguages: overrides.rest?.repos?.listLanguages as OctokitClient["rest"]["repos"]["listLanguages"] ??
+ jest.fn().mockResolvedValue({
+ data: { TypeScript: 5000, JavaScript: 2000 },
+ }),
+ getContent: overrides.rest?.repos?.getContent as OctokitClient["rest"]["repos"]["getContent"] ??
+ jest.fn().mockResolvedValue({
+ data: { sha: "abc123" },
+ }),
+ createOrUpdateFileContents: overrides.rest?.repos?.createOrUpdateFileContents as OctokitClient["rest"]["repos"]["createOrUpdateFileContents"] ??
+ jest.fn().mockResolvedValue({
+ data: { commit: { sha: "def456" } },
+ }),
+ },
+ },
+ createPullRequest: overrides.createPullRequest as OctokitClient["createPullRequest"] ??
+ jest.fn().mockResolvedValue({
+ data: { html_url: "https://github.com/test-owner/test-repo/pull/1" },
+ }),
+ };
+}
+
+// creates a full mock Dependencies object with sensible defaults
+export function createMockDeps(overrides: Partial = {}): Dependencies {
+ const mockOctokit = createMockOctokit();
+
+ return {
+ owner: "test-owner",
+ repo: "test-repo",
+ githubToken: "fake-github-token",
+ adminToken: "",
+ branch: "main",
+ skipPR: false,
+ isArchived: false,
+
+ octokit: mockOctokit,
+ adminOctokit: null,
+
+ exec: jest.fn().mockResolvedValue({
+ stdout: JSON.stringify({ estimatedScheduleMonths: 2.5 }),
+ stderr: "",
+ }),
+ readFile: jest.fn().mockRejectedValue(new Error("File not found")),
+
+ log: createMockLogger(),
+ setOutput: jest.fn(),
+ setFailed: jest.fn(),
+
+ ...overrides,
+ };
+}
+
+// helper type for deep partial overrides
+type DeepPartial = {
+ [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
+};
\ No newline at end of file
diff --git a/src/__tests__/unit/helper.test.ts b/src/__tests__/unit/helper.test.ts
new file mode 100644
index 00000000..368607f3
--- /dev/null
+++ b/src/__tests__/unit/helper.test.ts
@@ -0,0 +1,206 @@
+import { describe, it, expect, jest, beforeEach } from "@jest/globals";
+import { createHelpers } from "../../helper.js";
+import { createMockDeps, createMockOctokit } from "../fixtures/mock-deps.js";
+import { Dependencies } from "../../types/Dependencies.js";
+
+describe("createHelpers - calculateMetaData", () => {
+ let deps: Dependencies;
+
+ beforeEach(() => {
+ deps = createMockDeps();
+ });
+
+ it("returns metadata from GitHub API and SCC", async () => {
+ const helpers = createHelpers(deps);
+ const result = await helpers.calculateMetaData();
+
+ expect(result.name).toBe("test-repo");
+ expect(result.description).toBe("A test repository");
+ expect(result.repositoryURL).toBe("https://github.com/test-owner/test-repo");
+ expect(result.repositoryVisibility).toBe("public");
+ expect(result.languages).toEqual(["TypeScript", "JavaScript"]);
+ expect(result.laborHours).toBeGreaterThan(0);
+ expect(result.reuseFrequency?.forks).toBe(5);
+ expect(result.tags).toEqual(["test", "automation"]);
+ });
+
+ it("reports private visibility for private repos", async () => {
+ const mockOctokit = createMockOctokit({
+ rest: {
+ repos: {
+ get: jest.fn().mockResolvedValue({
+ data: {
+ name: "private-repo",
+ description: "Secret stuff",
+ html_url: "https://github.com/test-owner/private-repo",
+ private: true,
+ forks_count: 0,
+ topics: [],
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-06-01T00:00:00Z",
+ default_branch: "main",
+ },
+ }),
+ },
+ },
+ });
+
+ deps = createMockDeps({ octokit: mockOctokit });
+ const helpers = createHelpers(deps);
+ const result = await helpers.calculateMetaData();
+
+ expect(result.repositoryVisibility).toBe("private");
+ });
+
+ it("handles SCC failure gracefully", async () => {
+ deps = createMockDeps({
+ exec: jest.fn().mockRejectedValue(new Error("scc not found")),
+ });
+
+ const helpers = createHelpers(deps);
+ await expect(helpers.calculateMetaData()).rejects.toThrow("scc not found");
+ expect(deps.log.error).toHaveBeenCalled();
+ });
+});
+
+describe("createHelpers - getBaseBranch", () => {
+ it("returns configured branch when provided", async () => {
+ const deps = createMockDeps({ branch: "develop" });
+ const helpers = createHelpers(deps);
+
+ expect(await helpers.getBaseBranch()).toBe("develop");
+ });
+
+ it("fetches default branch from API when not configured", async () => {
+ const deps = createMockDeps({ branch: "" });
+ const helpers = createHelpers(deps);
+
+ expect(await helpers.getBaseBranch()).toBe("main");
+ expect(deps.octokit.rest.repos.get).toHaveBeenCalled();
+ });
+});
+
+describe("createHelpers - readJSON", () => {
+ it("parses valid JSON from file", async () => {
+ const mockData = { name: "test", version: "1.0" };
+ const deps = createMockDeps({
+ readFile: jest.fn().mockResolvedValue(JSON.stringify(mockData)),
+ });
+ const helpers = createHelpers(deps);
+
+ const result = await helpers.readJSON("/some/path/code.json");
+ expect(result).toEqual(mockData);
+ });
+
+ it("returns null for missing files", async () => {
+ const deps = createMockDeps({
+ readFile: jest.fn().mockRejectedValue(new Error("ENOENT")),
+ });
+ const helpers = createHelpers(deps);
+
+ const result = await helpers.readJSON("/missing/code.json");
+ expect(result).toBeNull();
+ });
+
+ it("returns null for invalid JSON", async () => {
+ const deps = createMockDeps({
+ readFile: jest.fn().mockResolvedValue("not json {{{"),
+ });
+ const helpers = createHelpers(deps);
+
+ const result = await helpers.readJSON("/bad/code.json");
+ expect(result).toBeNull();
+ });
+});
+
+describe("createHelpers - sendPR", () => {
+ it("creates a PR and sets outputs", async () => {
+ const deps = createMockDeps();
+ const helpers = createHelpers(deps);
+
+ const fakeCodeJSON = { name: "test" } as any;
+ await helpers.sendPR(fakeCodeJSON, "main");
+
+ expect(deps.octokit.createPullRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ owner: "test-owner",
+ repo: "test-repo",
+ base: "main",
+ title: "Update code.json",
+ }),
+ );
+ expect(deps.setOutput).toHaveBeenCalledWith("updated", true);
+ expect(deps.setOutput).toHaveBeenCalledWith("method_used", "pull_request");
+ });
+
+ it("uses archival title and labels when archived", async () => {
+ const deps = createMockDeps({ isArchived: true });
+ const helpers = createHelpers(deps);
+
+ await helpers.sendPR({ name: "test" } as any, "main");
+
+ expect(deps.octokit.createPullRequest).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: "Update code.json for archival",
+ labels: ["archived"],
+ }),
+ );
+ });
+});
+
+describe("createHelpers - pushDirectlyWithFallback", () => {
+ it("falls back to PR when no admin token", async () => {
+ const deps = createMockDeps({ adminToken: "" });
+ const helpers = createHelpers(deps);
+
+ await helpers.pushDirectlyWithFallback({ name: "test" } as any, "main");
+
+ // Should have fallen back to PR
+ expect(deps.octokit.createPullRequest).toHaveBeenCalled();
+ expect(deps.log.error).toHaveBeenCalledWith(
+ expect.stringContaining("ADMIN_TOKEN is not provided"),
+ );
+ });
+
+ it("pushes directly when admin token is available", async () => {
+ const adminOctokit = createMockOctokit();
+ const deps = createMockDeps({
+ adminToken: "admin-pat-token",
+ adminOctokit,
+ });
+ const helpers = createHelpers(deps);
+
+ await helpers.pushDirectlyWithFallback({ name: "test" } as any, "main");
+
+ expect(adminOctokit.rest.repos.createOrUpdateFileContents).toHaveBeenCalled();
+ expect(deps.setOutput).toHaveBeenCalledWith("method_used", "direct_push");
+ });
+});
+
+describe("createHelpers - validateOnly", () => {
+ it("fails when code.json is missing", async () => {
+ const deps = createMockDeps({
+ readFile: jest.fn().mockRejectedValue(new Error("ENOENT")),
+ });
+ const helpers = createHelpers(deps);
+
+ await helpers.validateOnly();
+
+ expect(deps.setFailed).toHaveBeenCalledWith(
+ expect.stringContaining("code.json file not found"),
+ );
+ });
+
+ it("succeeds for valid code.json", async () => {
+ const validCodeJSON = await import("../fixtures/test-code.json");
+ const deps = createMockDeps({
+ readFile: jest.fn().mockResolvedValue(JSON.stringify(validCodeJSON.default ?? validCodeJSON)),
+ });
+ const helpers = createHelpers(deps);
+
+ await helpers.validateOnly();
+
+ expect(deps.setFailed).not.toHaveBeenCalled();
+ expect(deps.log.info).toHaveBeenCalledWith("code.json is valid!");
+ });
+});
\ No newline at end of file
diff --git a/src/__tests__/unit/main.test.ts b/src/__tests__/unit/main.test.ts
new file mode 100644
index 00000000..39a5b186
--- /dev/null
+++ b/src/__tests__/unit/main.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect, jest, beforeEach, afterEach } from "@jest/globals";
+import { runWithDeps, filterValidFields, getMetaData } from "../../main.js";
+import { createHelpers } from "../../helper.js";
+import { createMockDeps } from "../fixtures/mock-deps.js";
+import validCodeJSON from "../fixtures/test-code.json";
+
+describe("filterValidFields", () => {
+ it("keeps known fields", () => {
+ const result = filterValidFields({ name: "test", version: "1.0", description: "hi" });
+ expect(result).toHaveProperty("name", "test");
+ expect(result).toHaveProperty("version", "1.0");
+ });
+
+ it("strips unknown fields", () => {
+ const result = filterValidFields({ name: "test", unknownField: "bad" });
+ expect(result).toHaveProperty("name");
+ expect(result).not.toHaveProperty("unknownField");
+ });
+});
+
+describe("getMetaData", () => {
+ it("preserves existing feedbackMechanism", async () => {
+ const deps = createMockDeps();
+ const helpers = createHelpers(deps);
+
+ const existing = { ...validCodeJSON, feedbackMechanism: "https://custom.example.com/feedback" } as any;
+ const result = await getMetaData(helpers, deps, existing);
+
+ expect(result.feedbackMechanism).toBe("https://custom.example.com/feedback");
+ });
+
+ it("defaults feedbackMechanism to issues URL", async () => {
+ const deps = createMockDeps();
+ const helpers = createHelpers(deps);
+
+ const result = await getMetaData(helpers, deps, null);
+
+ expect(result.feedbackMechanism).toContain("/issues");
+ });
+
+ it("sets Archival status when isArchived", async () => {
+ const deps = createMockDeps({ isArchived: true });
+ const helpers = createHelpers(deps);
+
+ const result = await getMetaData(helpers, deps, validCodeJSON as any);
+
+ expect(result.status).toBe("Archival");
+ expect(result.tags).toContain("archived");
+ });
+
+ it("converts legacy string contractNumber to array", async () => {
+ const deps = createMockDeps();
+ const helpers = createHelpers(deps);
+
+ const existing = { ...validCodeJSON, contractNumber: "LEGACY-001" } as any;
+ const result = await getMetaData(helpers, deps, existing);
+
+ expect(result.contractNumber).toEqual(["LEGACY-001"]);
+ });
+});
+
+describe("runWithDeps", () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ it("validates only on pull_request events", async () => {
+ process.env.GITHUB_EVENT_NAME = "pull_request";
+
+ const deps = createMockDeps({
+ readFile: jest.fn().mockResolvedValue(JSON.stringify(validCodeJSON)),
+ });
+
+ await runWithDeps(deps);
+
+ expect(deps.log.info).toHaveBeenCalledWith(
+ expect.stringContaining("validating only"),
+ );
+ // Should not attempt to create PR
+ expect(deps.octokit.createPullRequest).not.toHaveBeenCalled();
+ });
+
+ it("creates PR on schedule event", async () => {
+ process.env.GITHUB_EVENT_NAME = "schedule";
+
+ const deps = createMockDeps({
+ readFile: jest.fn().mockRejectedValue(new Error("no file")),
+ skipPR: false,
+ });
+
+ await runWithDeps(deps);
+
+ expect(deps.octokit.createPullRequest).toHaveBeenCalled();
+ expect(deps.setOutput).toHaveBeenCalledWith("method_used", "pull_request");
+ });
+
+ it("attempts direct push when skipPR is true with admin token", async () => {
+ process.env.GITHUB_EVENT_NAME = "workflow_dispatch";
+
+ const adminOctokit = {
+ rest: {
+ repos: {
+ get: jest.fn().mockResolvedValue({ data: { default_branch: "main" } }),
+ listLanguages: jest.fn().mockResolvedValue({ data: {} }),
+ getContent: jest.fn().mockResolvedValue({ data: { sha: "abc" } }),
+ createOrUpdateFileContents: jest.fn().mockResolvedValue({
+ data: { commit: { sha: "pushed123" } },
+ }),
+ },
+ },
+ createPullRequest: jest.fn(),
+ };
+
+ const deps = createMockDeps({
+ skipPR: true,
+ adminToken: "admin-token",
+ adminOctokit,
+ });
+
+ await runWithDeps(deps);
+
+ expect(adminOctokit.rest.repos.createOrUpdateFileContents).toHaveBeenCalled();
+ expect(deps.setOutput).toHaveBeenCalledWith("method_used", "direct_push");
+ });
+
+ it("falls back to PR when skipPR but no admin token", async () => {
+ process.env.GITHUB_EVENT_NAME = "schedule";
+
+ const deps = createMockDeps({
+ skipPR: true,
+ adminToken: "",
+ });
+
+ await runWithDeps(deps);
+
+ expect(deps.log.warning).toHaveBeenCalledWith(
+ expect.stringContaining("ADMIN_TOKEN is not provided"),
+ );
+ expect(deps.octokit.createPullRequest).toHaveBeenCalled();
+ });
+
+ it("sets failed on unexpected errors", async () => {
+ process.env.GITHUB_EVENT_NAME = "schedule";
+
+ const deps = createMockDeps({
+ exec: jest.fn().mockRejectedValue(new Error("catastrophic failure")),
+ });
+
+ await runWithDeps(deps);
+
+ expect(deps.setFailed).toHaveBeenCalledWith(
+ expect.stringContaining("Action failed"),
+ );
+ });
+});
\ No newline at end of file
diff --git a/src/create-deps.ts b/src/create-deps.ts
new file mode 100644
index 00000000..81b1f5c8
--- /dev/null
+++ b/src/create-deps.ts
@@ -0,0 +1,66 @@
+import * as core from "@actions/core";
+import * as fs from "fs/promises";
+import { Octokit as ActionKit } from "@octokit/action";
+import { createPullRequest } from "octokit-plugin-create-pull-request";
+import { exec } from "child_process";
+import { promisify } from "util";
+import { Dependencies } from "./types/Dependencies.js";
+
+const execAsync = promisify(exec);
+
+// builds the production level Dependencies object from the GitHub Actions environment
+export function createProductionDeps(): Dependencies {
+ const githubToken = core.getInput("GITHUB_TOKEN", { required: true });
+ const adminToken = core.getInput("ADMIN_TOKEN", { required: false });
+
+ const MyOctoKit = ActionKit.plugin(createPullRequest);
+
+ const octokit = new MyOctoKit({
+ auth: githubToken,
+ log: {
+ debug: core.debug,
+ info: core.info,
+ warn: core.warning,
+ error: core.error,
+ },
+ });
+
+ const adminOctokit = adminToken
+ ? new MyOctoKit({
+ auth: adminToken,
+ log: {
+ debug: core.debug,
+ info: core.info,
+ warn: core.warning,
+ error: core.error,
+ },
+ })
+ : null;
+
+ return {
+ owner: process.env.GITHUB_REPOSITORY_OWNER ?? "",
+ repo: process.env.GITHUB_REPOSITORY?.split("/")[1] ?? "",
+
+ githubToken,
+ adminToken,
+ branch: core.getInput("BRANCH", { required: false }),
+ skipPR: core.getInput("SKIP_PR", { required: false }) === "true",
+ isArchived: core.getInput("ARCHIVE", { required: false }) === "true",
+
+ octokit: octokit as unknown as Dependencies["octokit"],
+ adminOctokit: adminOctokit as unknown as Dependencies["adminOctokit"],
+
+ exec: execAsync,
+ readFile: (filepath: string) => fs.readFile(filepath, "utf8"),
+
+ log: {
+ info: core.info,
+ error: core.error,
+ warning: core.warning,
+ debug: core.debug,
+ },
+
+ setOutput: core.setOutput,
+ setFailed: core.setFailed,
+ };
+}
\ No newline at end of file
diff --git a/src/helper.ts b/src/helper.ts
index 785bdd7b..0bd5ccab 100644
--- a/src/helper.ts
+++ b/src/helper.ts
@@ -1,309 +1,284 @@
-import * as core from "@actions/core";
-import * as fs from "fs/promises";
-
-import { Octokit as ActionKit } from "@octokit/action";
-import { createPullRequest } from "octokit-plugin-create-pull-request";
-import { exec } from "child_process";
-import { promisify } from "util";
-
import { CodeJSON } from "./types/CodeJSONSchema.js";
import { BasicRepoInfo } from "./types/BasicRepoInfo.js";
import { validateCodeJSON } from "./zod-validation.js";
-
-const execAsync = promisify(exec);
-
-const TOKEN = core.getInput("GITHUB_TOKEN", { required: true });
-const ADMIN_TOKEN = core.getInput("ADMIN_TOKEN", { required: false });
-
-const MyOctoKit = ActionKit.plugin(createPullRequest);
-const octokit = new MyOctoKit({
- auth: TOKEN,
- log: {
- debug: core.debug,
- info: core.info,
- warn: core.warning,
- error: core.error,
- },
-});
-
-const adminOctokit = ADMIN_TOKEN
- ? new MyOctoKit({
- auth: ADMIN_TOKEN,
- log: {
- debug: core.debug,
- info: core.info,
- warn: core.warning,
- error: core.error,
- },
- })
- : null;
-
-const owner = process.env.GITHUB_REPOSITORY_OWNER ?? "";
-const repo = process.env.GITHUB_REPOSITORY?.split("/")[1] ?? "";
+import { Dependencies } from "./types/Dependencies.js";
const HOURS_PER_MONTH = 730.001;
-//===============================================
-// Meta Data
-//===============================================
-export async function calculateMetaData(): Promise> {
- try {
- const [laborHours, basicInfo] = await Promise.all([
- getLaborHours(),
- getBasicInfo(),
- ]);
-
- return {
- name: basicInfo.title,
- description: basicInfo.description,
- repositoryURL: basicInfo.url,
- repositoryVisibility: basicInfo.repositoryVisibility,
- laborHours: laborHours,
- languages: basicInfo.languages,
- reuseFrequency: {
- forks: basicInfo.forks,
- clones: 0,
- },
- tags: basicInfo.tags,
- date: {
- created: basicInfo.date.created,
- lastModified: basicInfo.date.lastModified,
- metadataLastUpdated: basicInfo.date.metadataLastUpdated,
- },
- };
- } catch (error) {
- core.error(`Failed to calculate meta data: ${error}`);
- throw error;
+export function createHelpers(deps: Dependencies) {
+ const { owner, repo, octokit, adminOctokit, log, setOutput, isArchived } = deps;
+
+ //===============================================
+ // Meta Data
+ //===============================================
+ async function calculateMetaData(): Promise> {
+ try {
+ const [laborHours, basicInfo] = await Promise.all([
+ getLaborHours(),
+ getBasicInfo(),
+ ]);
+
+ return {
+ name: basicInfo.title,
+ description: basicInfo.description,
+ repositoryURL: basicInfo.url,
+ repositoryVisibility: basicInfo.repositoryVisibility,
+ laborHours: laborHours,
+ languages: basicInfo.languages,
+ reuseFrequency: {
+ forks: basicInfo.forks,
+ clones: 0,
+ },
+ tags: basicInfo.tags,
+ date: {
+ created: basicInfo.date.created,
+ lastModified: basicInfo.date.lastModified,
+ metadataLastUpdated: basicInfo.date.metadataLastUpdated,
+ },
+ };
+ } catch (error) {
+ log.error(`Failed to calculate meta data: ${error}`);
+ throw error;
+ }
}
-}
-async function getBasicInfo(): Promise {
- try {
- const [repoData, languagesData] = await Promise.all([
- octokit.rest.repos.get({ owner, repo }),
- octokit.rest.repos.listLanguages({ owner, repo }),
- ]);
-
- const languages = Object.keys(languagesData.data);
- const topics = repoData.data.topics || [];
- const tags = topics.filter(
- (topic) => typeof topic === "string" && topic.trim() !== "",
- );
+ async function getBasicInfo(): Promise {
+ try {
+ const [repoData, languagesData] = await Promise.all([
+ octokit.rest.repos.get({ owner, repo }),
+ octokit.rest.repos.listLanguages({ owner, repo }),
+ ]);
+
+ const languages = Object.keys(languagesData.data);
+ const topics = repoData.data.topics || [];
+ const tags = topics.filter(
+ (topic) => typeof topic === "string" && topic.trim() !== "",
+ );
- return {
- title: repoData.data.name,
- description: repoData.data.description ?? "",
- url: repoData.data.html_url,
- repositoryVisibility: repoData.data.private ? "private" : "public",
- languages: languages,
- forks: repoData.data.forks_count,
- tags: tags,
- date: {
- created: repoData.data.created_at,
- lastModified: repoData.data.updated_at,
- metadataLastUpdated: new Date().toISOString(),
- },
- };
- } catch (error) {
- core.error(`Failed to get basic info: ${error}`);
- throw error;
+ return {
+ title: repoData.data.name,
+ description: repoData.data.description ?? "",
+ url: repoData.data.html_url,
+ repositoryVisibility: repoData.data.private ? "private" : "public",
+ languages: languages,
+ forks: repoData.data.forks_count,
+ tags: tags,
+ date: {
+ created: repoData.data.created_at,
+ lastModified: repoData.data.updated_at,
+ metadataLastUpdated: new Date().toISOString(),
+ },
+ };
+ } catch (error) {
+ log.error(`Failed to get basic info: ${error}`);
+ throw error;
+ }
}
-}
-async function getLaborHours(): Promise {
- try {
- const { stdout } = await execAsync(`scc /github/workspace --format json2`);
- const sccData = JSON.parse(stdout);
+ async function getLaborHours(): Promise {
+ try {
+ const { stdout } = await deps.exec(`scc /github/workspace --format json2`);
+ const sccData = JSON.parse(stdout);
- const laborHours = Math.ceil(
- sccData["estimatedScheduleMonths"] * HOURS_PER_MONTH,
- );
- return laborHours;
- } catch (error) {
- core.error(`Failed to get labor hours: ${error}`);
- throw error;
+ const laborHours = Math.ceil(
+ sccData["estimatedScheduleMonths"] * HOURS_PER_MONTH,
+ );
+ return laborHours;
+ } catch (error) {
+ log.error(`Failed to get labor hours: ${error}`);
+ throw error;
+ }
}
-}
-export async function getBaseBranch(): Promise {
- const BRANCH = core.getInput("BRANCH", { required: false });
+ async function getBaseBranch(): Promise {
+ if (deps.branch) {
+ return deps.branch;
+ }
- if (BRANCH) {
- return BRANCH;
- } else {
try {
const repoData = await octokit.rest.repos.get({ owner, repo });
return repoData.data.default_branch;
} catch (error) {
- core.error(`Failed to get Base Branch Name: ${error}`);
+ log.error(`Failed to get Base Branch Name: ${error}`);
throw error;
}
}
-}
-//===============================================
-// Validation
-//===============================================
-export async function validateOnly(): Promise {
- try {
- const codeJSON = await readJSON("/github/workspace/code.json");
+ //===============================================
+ // Validation
+ //===============================================
+ async function validateOnly(): Promise {
+ try {
+ const codeJSON = await readJSON("/github/workspace/code.json");
- if (!codeJSON) {
- core.setFailed(
- "code.json file not found, is empty, or contains invalid JSON syntax...",
- );
- return;
- }
+ if (!codeJSON) {
+ deps.setFailed(
+ "code.json file not found, is empty, or contains invalid JSON syntax...",
+ );
+ return;
+ }
- const validationErrors = validateCodeJSON(codeJSON);
+ const validationErrors = validateCodeJSON(codeJSON);
- if (validationErrors.length > 0) {
- const errorMessage = `code.json validation failed with ${validationErrors.length} error(s):\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`;
- core.setFailed(errorMessage);
- return;
- }
+ if (validationErrors.length > 0) {
+ const errorMessage = `code.json validation failed with ${validationErrors.length} error(s):\n\n${validationErrors.map((err, idx) => `${idx + 1}. ${err}`).join("\n")}`;
+ deps.setFailed(errorMessage);
+ return;
+ }
- core.info("code.json is valid!");
- } catch (error) {
- core.setFailed(`validation error: ${error}`);
+ log.info("code.json is valid!");
+ } catch (error) {
+ deps.setFailed(`validation error: ${error}`);
+ }
}
-}
-export { validateCodeJSON };
-
-//===============================================
-// Data Handling
-//===============================================
-export async function readJSON(filepath: string): Promise {
- try {
- const fileContent = await fs.readFile(filepath, "utf8");
- return JSON.parse(fileContent) as CodeJSON;
- } catch (error) {
- console.log(`Error with reading JSON file: ${error}`);
- return null;
+ //===============================================
+ // Data Handling
+ //===============================================
+ async function readJSON(filepath: string): Promise {
+ try {
+ const fileContent = await deps.readFile(filepath);
+ return JSON.parse(fileContent) as CodeJSON;
+ } catch (error) {
+ console.log(`Error with reading JSON file: ${error}`);
+ return null;
+ }
}
-}
-export async function sendPR(
- updatedCodeJSON: CodeJSON,
- baseBranchName: string,
-) {
- try {
- const formattedContent = JSON.stringify(updatedCodeJSON, null, 2) + "\n";
- const headBranchName = `code-json-${new Date().getTime()}`;
-
- const PR = await octokit.createPullRequest({
- owner,
- repo,
- title: "Update code.json",
- body: bodyOfPR(),
- base: baseBranchName,
- head: headBranchName,
- labels: ["codejson-initialized"],
- changes: [
- {
- files: {
- "code.json": formattedContent,
+ async function sendPR(
+ updatedCodeJSON: CodeJSON,
+ baseBranchName: string,
+ ) {
+ try {
+ const formattedContent = JSON.stringify(updatedCodeJSON, null, 2) + "\n";
+ const headBranchName = `code-json-${new Date().getTime()}`;
+
+ const PR = await octokit.createPullRequest({
+ owner,
+ repo,
+ title: isArchived ? "Update code.json for archival" : "Update code.json",
+ body: isArchived ? bodyOfArchivalPR() : bodyOfPR(),
+ base: baseBranchName,
+ head: headBranchName,
+ labels: isArchived ? ["archived"] : ["codejson-initialized"],
+ changes: [
+ {
+ files: {
+ "code.json": formattedContent,
+ },
+ commit: "Update code.json metadata",
},
- commit: "Update code.json metadata",
- },
- ],
- });
-
- if (PR) {
- core.info(`Successfully created PR: ${PR.data.html_url}`);
-
- core.setOutput("updated", true);
- core.setOutput("pr_url", PR.data.html_url);
- core.setOutput("method_used", "pull_request");
- } else {
- core.error(`Failed to create PR because of PR object`);
- core.setOutput("updated", false);
- }
- } catch (error) {
- core.error(`Failed to create PR: ${error}`);
- }
-}
+ ],
+ });
-async function pushDirectlyWithPAT(
- updatedCodeJSON: CodeJSON,
- baseBranchName: string,
-): Promise {
- if (!adminOctokit) {
- core.error("Admin token not provided for direct push");
- return false;
+ if (PR) {
+ log.info(`Successfully created PR: ${PR.data.html_url}`);
+
+ setOutput("updated", true);
+ setOutput("pr_url", PR.data.html_url);
+ setOutput("method_used", "pull_request");
+ } else {
+ log.error(`Failed to create PR because of PR object`);
+ setOutput("updated", false);
+ }
+ } catch (error) {
+ log.error(`Failed to create PR: ${error}`);
+ }
}
- try {
- const formattedContent = JSON.stringify(updatedCodeJSON, null, 2);
+ async function pushDirectlyWithPAT(
+ updatedCodeJSON: CodeJSON,
+ baseBranchName: string,
+ ): Promise {
+ if (!adminOctokit) {
+ log.error("Admin token not provided for direct push");
+ return false;
+ }
- let currentFileSha: string | undefined;
try {
- const currentFile = await adminOctokit.rest.repos.getContent({
+ const formattedContent = JSON.stringify(updatedCodeJSON, null, 2);
+
+ let currentFileSha: string | undefined;
+ try {
+ const currentFile = await adminOctokit.rest.repos.getContent({
+ owner,
+ repo,
+ path: "code.json",
+ ref: baseBranchName,
+ });
+
+ if ("sha" in currentFile.data) {
+ currentFileSha = currentFile.data.sha;
+ }
+ } catch (error) {
+ log.info(`code.json doesn't exist yet, will create new file ${error}`);
+ }
+
+ const result = await adminOctokit.rest.repos.createOrUpdateFileContents({
owner,
repo,
path: "code.json",
- ref: baseBranchName,
+ message: "Update code.json metadata",
+ content: Buffer.from(formattedContent).toString("base64"),
+ branch: baseBranchName,
+ sha: currentFileSha,
});
- if ("sha" in currentFile.data) {
- currentFileSha = currentFile.data.sha;
- }
+ log.info(`Successfully pushed commit with PAT: ${result.data.commit.sha}`);
+
+ setOutput("updated", true);
+ setOutput("commit_sha", result.data.commit.sha);
+ setOutput("method_used", "direct_push");
+ return true;
} catch (error) {
- core.info("code.json doesn't exist yet, will create new file");
+ log.error(`Failed to push directly with PAT: ${error}`);
+ return false;
}
-
- const result = await adminOctokit.rest.repos.createOrUpdateFileContents({
- owner,
- repo,
- path: "code.json",
- message: "Update code.json metadata",
- content: Buffer.from(formattedContent).toString("base64"),
- branch: baseBranchName,
- sha: currentFileSha,
- });
-
- core.info(`Successfully pushed commit with PAT: ${result.data.commit.sha}`);
-
- core.setOutput("updated", true);
- core.setOutput("commit_sha", result.data.commit.sha);
- core.setOutput("method_used", "direct_push");
- return true;
- } catch (error) {
- core.error(`Failed to push directly with PAT: ${error}`);
- return false;
}
-}
-
-export async function pushDirectlyWithFallback(
- updatedCodeJSON: CodeJSON,
- baseBranchName: string,
-) {
- if (!ADMIN_TOKEN) {
- core.error(
- "SKIP_PR is enabled but ADMIN_TOKEN is not provided. Direct push requires an admin PAT.",
- );
- core.info("Falling back to creating a pull request");
- await sendPR(updatedCodeJSON, baseBranchName);
- return;
- }
+ async function pushDirectlyWithFallback(
+ updatedCodeJSON: CodeJSON,
+ baseBranchName: string,
+ ) {
+ if (!deps.adminToken) {
+ log.error(
+ "SKIP_PR is enabled but ADMIN_TOKEN is not provided. Direct push requires an admin PAT.",
+ );
+ log.info("Falling back to creating a pull request");
- core.info("Attempting direct push with admin PAT!");
+ await sendPR(updatedCodeJSON, baseBranchName);
+ return;
+ }
- const directPushSuccess = await pushDirectlyWithPAT(
- updatedCodeJSON,
- baseBranchName,
- );
+ log.info("Attempting direct push with admin PAT!");
- if (!directPushSuccess) {
- core.info(
- "Direct push with PAT failed, falling back to creating a pull request",
+ const directPushSuccess = await pushDirectlyWithPAT(
+ updatedCodeJSON,
+ baseBranchName,
);
- await sendPR(updatedCodeJSON, baseBranchName);
+
+ if (!directPushSuccess) {
+ log.info(
+ "Direct push with PAT failed, falling back to creating a pull request",
+ );
+ await sendPR(updatedCodeJSON, baseBranchName);
+ }
}
+
+ return {
+ calculateMetaData,
+ getBaseBranch,
+ validateOnly,
+ validateCodeJSON,
+ readJSON,
+ sendPR,
+ pushDirectlyWithFallback,
+ };
}
+// export the type for convenience
+export type Helpers = ReturnType;
+
function bodyOfPR(): string {
return `
## Welcome to the Federal Open Source Community!
@@ -321,3 +296,23 @@ function bodyOfPR(): string {
If you would like additional information about the code.json metadata requirements, please visit the repository [here](https://github.com/DSACMS/gov-codejson).
`;
}
+
+function bodyOfArchivalPR(): string {
+ return `
+ ## Archiving the Repository
+
+ Hello, and thank you for your contributions to the Federal Open Source Community. 🙏
+
+ As part of preparing the repository for archival, this pull request marks the repository for archival and ensures code.json repository metadata is up-to-date.
+
+ If you have questions, please file an issue [here](https://github.com/DSACMS/automated-codejson-generator/issues) or join our #cms-ospo slack channel [here](https://cmsgov.enterprise.slack.com/archives/C07HM92S9QQ).
+
+ ## Next Steps
+ ### Verify project metadata in code.json
+ - Review all fields to ensure metadata is correct and accurate
+ - We have automatically updated some fields but some require manual input. Please update fields by directly editing code.json in Files Changed tab on your pull-request
+ - We also have a [form](https://dsacms.github.io/codejson-generator/) where you can create your code.json via a website, and then download directly to your local machine, and then you can copy and paste into here.
+
+ If you would like additional information about the code.json metadata requirements, please visit the repository [here](https://github.com/DSACMS/gov-codejson).
+ `;
+}
diff --git a/src/main.ts b/src/main.ts
index 8e03b478..0d0eeebf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,7 @@
-import * as core from "@actions/core";
import { CodeJSON } from "./types/CodeJSONSchema.js";
-import * as helpers from "./helper.js";
+import { Dependencies } from "./types/Dependencies.js";
+import { createHelpers, Helpers } from "./helper.js";
+import { createProductionDeps } from "./create-deps.js";
const baselineCodeJSON: Partial = {
name: "",
@@ -66,22 +67,28 @@ const baselineCodeJSON: Partial = {
maturityModelTier: 0,
};
-function filterValidFields(existingCodeJSON: any): Partial {
+export { baselineCodeJSON };
+
+function filterValidFields(existingCodeJSON: Record): Partial {
const validKeys = new Set(Object.keys(baselineCodeJSON));
- const filtered: any = {};
+ const filtered: Record = {};
for (const key of Object.keys(existingCodeJSON)) {
if (validKeys.has(key)) {
filtered[key] = existingCodeJSON[key];
} else {
- core.info(`Removing outdated field from current code.json: ${key}`);
+ console.log(`Removing outdated field from current code.json: ${key}`);
}
}
return filtered as Partial;
}
+export { filterValidFields };
+
async function getMetaData(
+ helpers: Helpers,
+ deps: Dependencies,
existingCodeJSON?: CodeJSON | null,
): Promise> {
const partialCodeJSON = await helpers.calculateMetaData();
@@ -112,7 +119,7 @@ async function getMetaData(
// handling legacy contractNumber that turned from string to array which caused validation errors
let contractNumber: string[] = [];
- const existingContract = existingCodeJSON?.contractNumber as any;
+ const existingContract: unknown = existingCodeJSON?.contractNumber;
if (existingContract) {
if (typeof existingContract === "string") {
contractNumber = existingContract.trim() ? [existingContract.trim()] : [];
@@ -122,12 +129,11 @@ async function getMetaData(
}
// handling archive option
- const isArchived = core.getInput("ARCHIVE", { required: false }) === "true";
let status = existingCodeJSON?.status || undefined;
- if (isArchived) {
+ if (deps.isArchived) {
status = "Archival";
- tags?.push("Archived");
+ tags?.push("archived");
}
return {
@@ -155,12 +161,16 @@ async function getMetaData(
};
}
-export async function run(): Promise {
+export { getMetaData };
+
+export async function runWithDeps(deps: Dependencies): Promise {
+ const helpers = createHelpers(deps);
+
try {
const eventName = process.env.GITHUB_EVENT_NAME;
if (eventName === "pull_request") {
- core.info("Detected pull_request event - validating only!");
+ deps.log.info("Detected pull_request event - validating only!");
await helpers.validateOnly();
return;
}
@@ -168,7 +178,7 @@ export async function run(): Promise {
const currentCodeJSON = await helpers.readJSON(
"/github/workspace/code.json",
);
- const metaData = await getMetaData(currentCodeJSON);
+ const metaData = await getMetaData(helpers, deps, currentCodeJSON);
let finalCodeJSON = {} as CodeJSON;
if (currentCodeJSON) {
@@ -187,30 +197,34 @@ export async function run(): Promise {
} as CodeJSON;
}
- core.info("Generated code.json successfully!");
+ deps.log.info("Generated code.json successfully!");
const baseBranchName = await helpers.getBaseBranch();
- const skipPR = core.getInput("SKIP_PR", { required: false }) === "true";
- const adminToken = core.getInput("ADMIN_TOKEN", { required: false });
- if (skipPR) {
- if (!adminToken) {
- core.warning("SKIP_PR is enabled but ADMIN_TOKEN is not provided.");
- core.warning(
+ if (deps.skipPR) {
+ if (!deps.adminToken) {
+ deps.log.warning("SKIP_PR is enabled but ADMIN_TOKEN is not provided.");
+ deps.log.warning(
"Direct push requires a Personal Access Token with appropriate permissions.",
);
- core.info("Falling back to pull request creation");
+ deps.log.info("Falling back to pull request creation");
await helpers.sendPR(finalCodeJSON, baseBranchName);
} else {
- core.info("Attempting direct push to branch");
+ deps.log.info("Attempting direct push to branch");
await helpers.pushDirectlyWithFallback(finalCodeJSON, baseBranchName);
}
} else {
- core.info("Creating pull request with updated code.json");
+ deps.log.info("Creating pull request with updated code.json");
await helpers.sendPR(finalCodeJSON, baseBranchName);
}
} catch (error) {
- core.setFailed(`Action failed: ${error}`);
+ deps.setFailed(`Action failed: ${error}`);
}
}
+
+// prod entry point
+export async function run(): Promise {
+ const deps = createProductionDeps();
+ return runWithDeps(deps);
+}
\ No newline at end of file
diff --git a/src/scripts/generate-schema.ts b/src/scripts/generate-schema.ts
index 1737920e..f3021c23 100644
--- a/src/scripts/generate-schema.ts
+++ b/src/scripts/generate-schema.ts
@@ -45,7 +45,7 @@ function addAdditionalRefinements(): string {
);
`;
- let refinements = permissionRefinement
+ const refinements = permissionRefinement
return refinements
}
diff --git a/src/types/Dependencies.ts b/src/types/Dependencies.ts
new file mode 100644
index 00000000..c00f470d
--- /dev/null
+++ b/src/types/Dependencies.ts
@@ -0,0 +1,89 @@
+// abstracting external dependencies
+export interface Dependencies {
+ owner: string;
+ repo: string;
+
+ githubToken: string;
+ adminToken: string;
+ branch: string;
+ skipPR: boolean;
+ isArchived: boolean;
+
+ octokit: OctokitClient;
+ adminOctokit: OctokitClient | null;
+
+ exec: (command: string) => Promise<{ stdout: string; stderr: string }>;
+ readFile: (filepath: string) => Promise;
+
+ log: Logger;
+
+ setOutput: (name: string, value: unknown) => void;
+ setFailed: (message: string) => void;
+}
+
+export interface Logger {
+ info: (message: string) => void;
+ error: (message: string) => void;
+ warning: (message: string) => void;
+ debug: (message: string) => void;
+}
+
+export interface OctokitClient {
+ rest: {
+ repos: {
+ get: (params: { owner: string; repo: string }) => Promise;
+ listLanguages: (params: { owner: string; repo: string }) => Promise;
+ getContent: (params: { owner: string; repo: string; path: string; ref: string }) => Promise;
+ createOrUpdateFileContents: (params: {
+ owner: string;
+ repo: string;
+ path: string;
+ message: string;
+ content: string;
+ branch: string;
+ sha?: string;
+ }) => Promise;
+ };
+ };
+ createPullRequest: (params: {
+ owner: string;
+ repo: string;
+ title: string;
+ body: string;
+ base: string;
+ head: string;
+ labels: string[];
+ changes: Array<{
+ files: Record;
+ commit: string;
+ }>;
+ }) => Promise<{ data: { html_url: string } } | null>;
+}
+
+export interface RepoResponse {
+ data: {
+ name: string;
+ description: string | null;
+ html_url: string;
+ private: boolean;
+ forks_count: number;
+ topics?: string[];
+ created_at: string;
+ updated_at: string;
+ default_branch: string;
+ };
+}
+
+export interface LanguagesResponse {
+ data: Record;
+}
+
+export interface ContentResponse {
+ data: { sha?: string } | Array;
+}
+
+export interface CommitResponse {
+ data: {
+ commit: { sha: string };
+ };
+}
\ No newline at end of file
diff --git a/src/zod-validation.ts b/src/zod-validation.ts
index 2f85b008..75a7017d 100644
--- a/src/zod-validation.ts
+++ b/src/zod-validation.ts
@@ -8,7 +8,7 @@ z.config({
}),
});
-export function validateCodeJSON(codeJSON: any): string[] {
+export function validateCodeJSON(codeJSON: unknown): string[] {
const result = CodeJSONSchema.safeParse(codeJSON);
if (result.success) {