From 369f31155399c6f08064a9f979fa27d7c76e3f90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:38:08 +0000 Subject: [PATCH] build(deps): Bump the github-actions-dependencies group across 8 directories with 11 updates Bumps the github-actions-dependencies group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [hoverkraft-tech/ci-github-common/.github/workflows/linter.yml](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [hoverkraft-tech/ci-github-common/.github/workflows/stale.yml](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `6.0.3` | | [hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml](https://github.com/hoverkraft-tech/ci-github-nodejs) | `0.24.2` | `0.24.3` | | [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common) | `0.35.4` | `0.36.0` | | [hoverkraft-tech/ci-github-container](https://github.com/hoverkraft-tech/ci-github-container) | `0.36.1` | `0.36.2` | | [hoverkraft-tech/ci-github-container/.github/workflows/docker-build-images.yml](https://github.com/hoverkraft-tech/ci-github-container) | `0.36.1` | `0.36.2` | Bumps the github-actions-dependencies group with 1 update in the /actions/deploy/get-environment directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/deploy/github-pages directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/deploy/report directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/release/create directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/release/get-configuration directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/release/plan directory: [hoverkraft-tech/ci-github-common](https://github.com/hoverkraft-tech/ci-github-common). Bumps the github-actions-dependencies group with 1 update in the /actions/release/summarize-changelog directory: [hoverkraft-tech/ci-github-nodejs](https://github.com/hoverkraft-tech/ci-github-nodejs). Updates `hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common/.github/workflows/linter.yml` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common/.github/workflows/stale.yml` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) Updates `hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml` from 0.24.2 to 0.24.3 - [Release notes](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-nodejs/compare/6b74a8f070140f5c120f78026d58e4c00d1b1e37...37a362e984a1841a2be36e2f85b68755dc01d056) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-container` from 0.36.1 to 0.36.2 - [Release notes](https://github.com/hoverkraft-tech/ci-github-container/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-container/compare/77f98ab8773b824eca7ed3f94e3e9c8b8af5875c...5396e1258d209f9af18e55da8692361508e3338c) Updates `hoverkraft-tech/ci-github-container/.github/workflows/docker-build-images.yml` from 0.36.1 to 0.36.2 - [Release notes](https://github.com/hoverkraft-tech/ci-github-container/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-container/compare/77f98ab8773b824eca7ed3f94e3e9c8b8af5875c...5396e1258d209f9af18e55da8692361508e3338c) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-common` from 0.35.4 to 0.36.0 - [Release notes](https://github.com/hoverkraft-tech/ci-github-common/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-common/compare/b553a696531fbd36743ccbb0c76c717971b8acdb...4bb7594b1bf3696c54b2bbae970376056853f8ea) Updates `hoverkraft-tech/ci-github-nodejs` from 0.24.2 to 0.24.3 - [Release notes](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) - [Commits](https://github.com/hoverkraft-tech/ci-github-nodejs/compare/6b74a8f070140f5c120f78026d58e4c00d1b1e37...37a362e984a1841a2be36e2f85b68755dc01d056) --- updated-dependencies: - dependency-name: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml dependency-version: 0.24.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-container dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-container/.github/workflows/docker-build-images.yml dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-common dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies - dependency-name: hoverkraft-tech/ci-github-nodejs dependency-version: 0.24.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies ... Signed-off-by: dependabot[bot] Signed-off-by: Emilien Escalle --- .devcontainer/devcontainer.json | 44 +- .github/dependabot.yml | 8 + .github/linters/.codespellrc | 5 + .github/linters/.jscpd.json | 4 +- .github/workflows/__greetings.yml | 4 +- .github/workflows/__main-ci.yml | 1 - .github/workflows/__need-fix-to-issue.yml | 2 +- .github/workflows/__pull-request-ci.yml | 1 - .github/workflows/__semantic-pull-request.yml | 4 +- .github/workflows/__shared-ci.yml | 4 +- .github/workflows/__stale.yml | 2 +- .../__test-action-check-url-lighthouse.yml | 2 +- .../__test-action-check-url-ping.yml | 37 +- ...st-action-deploy-argocd-manifest-files.yml | 2 +- .../__test-action-deploy-jekyll-jampack.yml | 12 +- .../workflows/__test-action-deployment.yml | 25 +- .../__test-action-release-create.yml | 2 +- ..._test-action-release-get-configuration.yml | 2 +- .../workflows/__test-action-release-plan.yml | 2 +- ...est-action-release-summarize-changelog.yml | 11 +- .../clean-deploy-argocd-app-of-apps.yml | 24 +- .github/workflows/clean-deploy.yml | 33 +- .../workflows/deploy-argocd-app-of-apps.md | 2 +- .../workflows/deploy-argocd-app-of-apps.yml | 32 +- .github/workflows/deploy-chart.md | 4 +- .github/workflows/deploy-chart.yml | 18 +- .github/workflows/deploy-checks.yml | 2 +- .github/workflows/deploy-finish.yml | 8 +- .github/workflows/deploy-start.md | 2 +- .github/workflows/deploy-start.yml | 6 +- .../finish-deploy-argocd-app-of-apps.yml | 31 +- .github/workflows/prepare-release.yml | 6 +- .github/workflows/release-actions.yml | 14 +- Dockerfile | 10 +- Makefile | 7 +- actions/argocd/get-manifest-files/action.yml | 34 +- actions/check/url-lighthouse/action.yml | 7 +- actions/check/url-ping/action.yml | 19 +- actions/check/url-ping/index.js | 672 ++++---- .../deploy/argocd-manifest-files/action.yml | 94 +- actions/deploy/get-environment/README.md | 2 +- actions/deploy/get-environment/action.yml | 13 +- actions/deploy/github-pages/action.yml | 16 +- .../helm-repository-dispatch/action.yml | 4 +- actions/deploy/jampack/action.yml | 9 +- actions/deploy/jekyll/action.yml | 20 +- actions/deploy/jekyll/asset-manager.js | 432 +++--- actions/deploy/jekyll/asset-manager.test.js | 160 +- actions/deploy/jekyll/package-lock.json | 28 +- actions/deploy/jekyll/package.json | 36 +- actions/deploy/jekyll/page-files.js | 217 +-- actions/deploy/jekyll/page-files.test.js | 160 +- actions/deploy/jekyll/prepare-site.js | 428 ++--- actions/deploy/jekyll/prepare-site.test.js | 266 ++-- actions/deploy/jekyll/site-file-manager.js | 22 +- .../deploy/jekyll/workspace-path-resolver.js | 92 +- .../jekyll/workspace-path-resolver.test.js | 84 +- actions/deploy/report/action.yml | 11 +- actions/deploy/report/scripts/context.js | 64 +- .../report/scripts/get-deployment-result.js | 108 +- actions/deploy/report/scripts/result.js | 142 +- actions/deploy/report/scripts/summary.js | 300 ++-- actions/deployment/get-finished/action.yml | 10 +- actions/deployment/read/action.yml | 9 +- actions/deployment/update/action.yml | 18 +- actions/release/create/action.yml | 10 +- actions/release/get-configuration/action.yml | 4 +- actions/release/plan/action.yml | 6 +- .../release/summarize-changelog/action.yml | 2 +- .../summarize-changelog/package-lock.json | 1380 ++++++++--------- .../release/summarize-changelog/package.json | 58 +- .../src/FileSystemService.js | 46 +- .../src/GitEvidenceService.js | 260 ++-- .../src/GitEvidenceService.test.js | 234 +-- .../src/LinkEvidenceService.js | 142 +- .../src/LinkEvidenceService.test.js | 66 +- .../src/LlmSummaryService.js | 284 ++-- .../src/LlmSummaryService.test.js | 130 +- .../summarize-changelog/src/LoggerService.js | 18 +- .../summarize-changelog/src/PromptBuilder.js | 350 ++--- .../src/ReferenceExtractor.js | 58 +- .../src/ReleaseSummaryCore.js | 294 ++-- .../src/ReleaseSummaryCore.test.js | 372 ++--- .../release/summarize-changelog/src/index.js | 62 +- .../summarize-changelog/src/index.test.js | 100 +- .../test-app-single-source/expected.yml | 16 +- .../test-app-single-source/template.yml.tpl | 16 +- .../ci/apps/ci-test/test-app/expected.yml | 20 +- .../ci/apps/ci-test/test-app/template.yml.tpl | 20 +- .../test-app-single-source/expected.yml | 4 +- .../test-app-single-source/template.yml.tpl | 4 +- .../manifests/ci-test/test-app/expected.yml | 4 +- .../ci-test/test-app/template.yml.tpl | 4 +- 93 files changed, 3964 insertions(+), 3850 deletions(-) create mode 100644 .github/linters/.codespellrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0159c5a..f992da2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,24 @@ { - "name": "Alpine", - "image": "mcr.microsoft.com/devcontainers/base:alpine-3.21", - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:3": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers-extra/features/act:1": {} - }, - "customizations": { - "vscode": { - "extensions": [ - "eamodio.gitlens", - "github.vscode-github-actions", - "github.copilot", - "github.copilot-chat", - "ms-vscode.makefile-tools", - "esbenp.prettier-vscode" - ], - "settings": { - "terminal.integrated.defaultProfile.linux": "zsh" - } - } - } + "name": "Alpine", + "image": "mcr.microsoft.com/devcontainers/base:alpine-3.21", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:3": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers-extra/features/act:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "eamodio.gitlens", + "github.vscode-github-actions", + "github.copilot", + "github.copilot-chat", + "ms-vscode.makefile-tools", + "esbenp.prettier-vscode" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh" + } + } + } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 657e45e..fb72cd2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: interval: weekly day: friday time: "04:00" + cooldown: + default-days: 7 groups: docker-dependencies: patterns: @@ -21,6 +23,8 @@ updates: interval: weekly day: friday time: "04:00" + cooldown: + default-days: 7 groups: github-actions-dependencies: patterns: @@ -34,6 +38,8 @@ updates: interval: weekly day: friday time: "04:00" + cooldown: + default-days: 7 groups: npm-dependencies: patterns: @@ -46,3 +52,5 @@ updates: interval: weekly day: friday time: "04:00" + cooldown: + default-days: 7 diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc new file mode 100644 index 0000000..fc15037 --- /dev/null +++ b/.github/linters/.codespellrc @@ -0,0 +1,5 @@ +[codespell] +skip = *.svg +ignore-words-list = + # commitish is the input name used by release-drafter + commitish, \ No newline at end of file diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 4d0a0c0..3958da9 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -1,4 +1,4 @@ { - "threshold": 5, - "ignore": ["**/tests/**"] + "threshold": 5, + "ignore": ["**/tests/**"] } diff --git a/.github/workflows/__greetings.yml b/.github/workflows/__greetings.yml index ef05e02..e27ac63 100644 --- a/.github/workflows/__greetings.yml +++ b/.github/workflows/__greetings.yml @@ -3,14 +3,14 @@ name: Greetings on: issues: types: [opened] - pull_request_target: + pull_request: branches: [main] permissions: {} jobs: greetings: - uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 permissions: contents: read issues: write diff --git a/.github/workflows/__main-ci.yml b/.github/workflows/__main-ci.yml index ae44969..8838061 100644 --- a/.github/workflows/__main-ci.yml +++ b/.github/workflows/__main-ci.yml @@ -29,7 +29,6 @@ jobs: pull-requests: write security-events: write statuses: write - secrets: inherit release: needs: ci diff --git a/.github/workflows/__need-fix-to-issue.yml b/.github/workflows/__need-fix-to-issue.yml index c4cebe7..c932c22 100644 --- a/.github/workflows/__need-fix-to-issue.yml +++ b/.github/workflows/__need-fix-to-issue.yml @@ -21,7 +21,7 @@ permissions: {} jobs: main: - uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 permissions: contents: read issues: write diff --git a/.github/workflows/__pull-request-ci.yml b/.github/workflows/__pull-request-ci.yml index f919d1f..71f0d26 100644 --- a/.github/workflows/__pull-request-ci.yml +++ b/.github/workflows/__pull-request-ci.yml @@ -24,4 +24,3 @@ jobs: pull-requests: write security-events: write statuses: write - secrets: inherit diff --git a/.github/workflows/__semantic-pull-request.yml b/.github/workflows/__semantic-pull-request.yml index b060784..41a6993 100644 --- a/.github/workflows/__semantic-pull-request.yml +++ b/.github/workflows/__semantic-pull-request.yml @@ -2,7 +2,7 @@ name: "Pull Request - Semantic Lint" on: - pull_request_target: + pull_request: types: - opened - edited @@ -12,7 +12,7 @@ permissions: {} jobs: main: - uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 permissions: contents: write pull-requests: write diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index 1c6dbd3..8e792ac 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -8,7 +8,7 @@ permissions: {} jobs: linter: - uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 permissions: actions: read contents: read @@ -36,7 +36,6 @@ jobs: test-action-deploy-jekyll-jampack: needs: linter uses: ./.github/workflows/__test-action-deploy-jekyll-jampack.yml - secrets: inherit permissions: contents: read id-token: write @@ -51,7 +50,6 @@ jobs: contents: read deployments: write pull-requests: read - secrets: inherit test-action-release-get-configuration: needs: linter diff --git a/.github/workflows/__stale.yml b/.github/workflows/__stale.yml index 49466c4..b31225f 100644 --- a/.github/workflows/__stale.yml +++ b/.github/workflows/__stale.yml @@ -9,7 +9,7 @@ permissions: {} jobs: main: - uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 permissions: issues: write pull-requests: write diff --git a/.github/workflows/__test-action-check-url-lighthouse.yml b/.github/workflows/__test-action-check-url-lighthouse.yml index 6c64717..3c54adf 100644 --- a/.github/workflows/__test-action-check-url-lighthouse.yml +++ b/.github/workflows/__test-action-check-url-lighthouse.yml @@ -13,7 +13,7 @@ jobs: contents: read steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/__test-action-check-url-ping.yml b/.github/workflows/__test-action-check-url-ping.yml index 589a226..bc0f681 100644 --- a/.github/workflows/__test-action-check-url-ping.yml +++ b/.github/workflows/__test-action-check-url-ping.yml @@ -23,7 +23,7 @@ jobs: - 1080:8080 steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -109,12 +109,14 @@ jobs: STEP_ID: test-success EXPECTED_STATUS: "200" TEST_NAME: "Test 1 - Successful URL check" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} + ATTEMPT_COUNT: ${{ steps[env.STEP_ID].outputs.attempt-count }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS}, got: ${statusCode}`); - const attemptCountRaw = ${{ toJSON(steps[env.STEP_ID].outputs.attempt-count) }}; + const attemptCountRaw = process.env.ATTEMPT_COUNT; if (attemptCountRaw === null) { throw new Error('Attempt count output missing'); } @@ -138,10 +140,11 @@ jobs: STEP_ID: test-multiple-status EXPECTED_STATUS: "404" TEST_NAME: "Test 2 - Multiple expected status codes" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS}, got: ${statusCode}`); console.log(`✅ ${process.env.TEST_NAME} passed`); @@ -161,9 +164,10 @@ jobs: env: STEP_ID: test-timeout TEST_NAME: "Test 3 - Timeout handling" + OUTCOME: ${{ steps[env.STEP_ID].outcome }} with: script: | - const outcome = `${{ steps[env.STEP_ID].outcome }}`; + const outcome = process.env.OUTCOME; if (outcome !== 'failure') { throw new Error(`Expected ${process.env.TEST_NAME} to fail, but outcome was: ${outcome}`); } @@ -268,13 +272,14 @@ jobs: STEP_ID: test-retry EXPECTED_STATUS: "200" TEST_NAME: "Test 4 - Retry with exponential backoff" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS} after retry, got: ${statusCode}`); console.log(`✅ ${process.env.TEST_NAME} passed`); - const attemptCountRaw = ${{ toJSON(steps[env.STEP_ID].outputs.attempt-count) }}; + const attemptCountRaw = process.env.ATTEMPT_COUNT; if (attemptCountRaw === null) { throw new Error('Attempt count output missing'); } @@ -298,10 +303,11 @@ jobs: STEP_ID: test-redirect-disabled EXPECTED_STATUS: "301" TEST_NAME: "Test 5 - Redirect handling (follow disabled)" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS}, got: ${statusCode}`); console.log(`✅ ${process.env.TEST_NAME} passed`); @@ -322,10 +328,11 @@ jobs: STEP_ID: test-redirect-enabled EXPECTED_STATUS: "200" TEST_NAME: "Test 6 - Redirect handling (follow enabled)" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS}, got: ${statusCode}`); console.log(`✅ ${process.env.TEST_NAME} passed`); @@ -345,9 +352,10 @@ jobs: env: STEP_ID: test-invalid-url TEST_NAME: "Test 7 - Invalid URL handling" + OUTCOME: ${{ steps[env.STEP_ID].outcome }} with: script: | - const outcome = `${{ steps[env.STEP_ID].outcome }}`; + const outcome = process.env.OUTCOME; if (outcome !== 'failure') { throw new Error(`Expected ${process.env.TEST_NAME} to fail, but outcome was: ${outcome}`); } @@ -369,10 +377,11 @@ jobs: STEP_ID: test-real-world EXPECTED_STATUS: "200" TEST_NAME: "Test 8 - Real-world endpoint check" + STATUS_CODE: ${{ steps[env.STEP_ID].outputs.status-code }} with: script: | const assert = require("assert"); - const statusCode = ${{ toJSON(steps[env.STEP_ID].outputs.status-code) }}; + const statusCode = process.env.STATUS_CODE; assert.equal(statusCode, process.env.EXPECTED_STATUS, `Expected ${process.env.EXPECTED_STATUS}, got: ${statusCode}`); console.log(`✅ ${process.env.TEST_NAME} passed`); @@ -436,13 +445,15 @@ jobs: STEP_ID: test-max-retries TEST_NAME: "Test 9 - Max retries exhausted" EXPECTED_MIN_ATTEMPTS: "2" + OUTCOME: ${{ steps[env.STEP_ID].outcome }} + ATTEMPT_COUNT: ${{ steps[env.STEP_ID].outputs.attempt-count }} with: script: | - const outcome = `${{ steps[env.STEP_ID].outcome }}`; + const outcome = process.env.OUTCOME; if (outcome !== 'failure') { throw new Error(`Expected ${process.env.TEST_NAME} to fail, but outcome was: ${outcome}`); } - const attemptCountRaw = ${{ toJSON(steps[env.STEP_ID].outputs.attempt-count) }}; + const attemptCountRaw = process.env.ATTEMPT_COUNT; if (attemptCountRaw === null) { throw new Error('Attempt count output missing'); } diff --git a/.github/workflows/__test-action-deploy-argocd-manifest-files.yml b/.github/workflows/__test-action-deploy-argocd-manifest-files.yml index c390537..5a92f6e 100644 --- a/.github/workflows/__test-action-deploy-argocd-manifest-files.yml +++ b/.github/workflows/__test-action-deploy-argocd-manifest-files.yml @@ -27,7 +27,7 @@ jobs: TEST_MANIFEST_FILE: tests/argocd-app-of-apps/ci/manifests/ci-test/${{ matrix.test-dir }}/test-app.yml TEST_EXPECTED_MANIFEST_FILE: tests/argocd-app-of-apps/ci/manifests/ci-test/${{ matrix.test-dir }}/expected.yml steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/__test-action-deploy-jekyll-jampack.yml b/.github/workflows/__test-action-deploy-jekyll-jampack.yml index a25656a..6a9ca1d 100644 --- a/.github/workflows/__test-action-deploy-jekyll-jampack.yml +++ b/.github/workflows/__test-action-deploy-jekyll-jampack.yml @@ -8,7 +8,7 @@ permissions: {} jobs: continuous-integration: name: Continuous integration for "deploy/jekyll" action - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@6b74a8f070140f5c120f78026d58e4c00d1b1e37 # 0.24.2 + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@df348077afa4e79725151d50606e9dc63f86dcb6 # 0.24.4 permissions: contents: read id-token: write @@ -30,7 +30,7 @@ jobs: build-path: ${{ steps.deploy-jekyll.outputs.build-path }} steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -61,13 +61,15 @@ jobs: - name: Assert - Check outputs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + BUILD_PATH: ${{ steps.deploy-jekyll.outputs.build-path }} with: script: | const assert = require("assert"); const { existsSync, readFileSync } = require("fs"); const path = require("path"); - const buildPathOutput = ${{ toJSON(steps.deploy-jekyll.outputs.build-path) }}; + const buildPathOutput = process.env.BUILD_PATH; assert(buildPathOutput, `"build-path" output is empty`); const workspacePath = process.env.GITHUB_WORKSPACE; @@ -131,13 +133,15 @@ jobs: - name: Assert - Check packed assets uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + BUILD_PATH: ${{ steps.deploy-jekyll.outputs.build-path }} with: # jscpd:ignore-start script: | const assert = require("assert"); const { existsSync } = require("fs"); - const buildPathOutput = ${{ toJSON(steps.deploy-jekyll.outputs.build-path) }}; + const buildPathOutput = process.env.BUILD_PATH; assert(buildPathOutput, `"build-path" output is empty`); // Check if the build assets path exists diff --git a/.github/workflows/__test-action-deployment.yml b/.github/workflows/__test-action-deployment.yml index 562174e..ae7caae 100644 --- a/.github/workflows/__test-action-deployment.yml +++ b/.github/workflows/__test-action-deployment.yml @@ -14,7 +14,7 @@ jobs: deployments: write pull-requests: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -24,8 +24,10 @@ jobs: environment: "review-apps" - name: Check create outputs + env: + DEPLOYMENT_ID: ${{ steps.create-deployment.outputs.deployment-id }} run: | - if [ -z "${{ steps.create-deployment.outputs.deployment-id }}" ]; then + if [ -z "${DEPLOYMENT_ID}" ]; then echo "Create deployment id output is not set" exit 1 fi @@ -37,18 +39,21 @@ jobs: repository: ${{ github.event.repository.name }} - name: Check get outputs + env: + ENVIRONMENT: ${{ steps.get-deployment.outputs.environment }} + URL: ${{ steps.get-deployment.outputs.url }} run: | - if [ -z "${{ steps.get-deployment.outputs.environment }}" ]; then + if [ -z "${ENVIRONMENT}" ]; then echo "Get deployment environment output is not set" exit 1 fi - if [ "${{ steps.get-deployment.outputs.environment }}" != "review-apps" ]; then - echo "Get deployment environment output is not 'review-apps': '${{ steps.get-deployment.outputs.environment }}'" + if [ "${ENVIRONMENT}" != "review-apps" ]; then + echo "Get deployment environment output is not 'review-apps': '${ENVIRONMENT}'" exit 1 fi - if [ -z "${{ steps.get-deployment.outputs.url }}" ]; then + if [ -z "${URL}" ]; then echo "Get deployment url output is not set" exit 1 fi @@ -62,8 +67,8 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: generate-token with: - client-id: ${{ vars.CI_BOT_APP_ID }} - private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} + client-id: ${{ vars.CI_BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional - id: delete-deployment uses: ./actions/deployment/delete @@ -71,8 +76,10 @@ jobs: token: ${{ steps.generate-token.outputs.token }} - name: Check delete outputs + env: + DEPLOYMENT_IDS: ${{ steps.delete-deployment.outputs.deployment-ids }} run: | - if [ -z "${{ steps.delete-deployment.outputs.deployment-ids }}" ]; then + if [ -z "${DEPLOYMENT_IDS}" ]; then echo "Delete deployment ids output is not set" exit 1 fi diff --git a/.github/workflows/__test-action-release-create.yml b/.github/workflows/__test-action-release-create.yml index fad9058..c50f7b3 100644 --- a/.github/workflows/__test-action-release-create.yml +++ b/.github/workflows/__test-action-release-create.yml @@ -14,7 +14,7 @@ jobs: pull-requests: read steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/__test-action-release-get-configuration.yml b/.github/workflows/__test-action-release-get-configuration.yml index 096ecc3..9d95c85 100644 --- a/.github/workflows/__test-action-release-get-configuration.yml +++ b/.github/workflows/__test-action-release-get-configuration.yml @@ -13,7 +13,7 @@ jobs: contents: read steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/__test-action-release-plan.yml b/.github/workflows/__test-action-release-plan.yml index 0fbf10f..c4736d5 100644 --- a/.github/workflows/__test-action-release-plan.yml +++ b/.github/workflows/__test-action-release-plan.yml @@ -14,7 +14,7 @@ jobs: pull-requests: read steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/__test-action-release-summarize-changelog.yml b/.github/workflows/__test-action-release-summarize-changelog.yml index 0c76cf6..777b07e 100644 --- a/.github/workflows/__test-action-release-summarize-changelog.yml +++ b/.github/workflows/__test-action-release-summarize-changelog.yml @@ -8,7 +8,7 @@ permissions: {} jobs: continuous-integration: name: Continuous integration for "release/summarize-changelog" action - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@6b74a8f070140f5c120f78026d58e4c00d1b1e37 # 0.24.2 + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@df348077afa4e79725151d50606e9dc63f86dcb6 # 0.24.4 permissions: contents: read id-token: write @@ -33,7 +33,7 @@ jobs: - 11434:8080 steps: - name: Arrange - Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -186,6 +186,9 @@ jobs: - name: Assert - Validate summarized changelog uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SUMMARY: ${{ steps.summarize.outputs.summary }} + LLM_PROMPT: ${{ steps.summarize.outputs.llm-prompt }} with: script: | const assert = require('node:assert/strict'); @@ -233,8 +236,8 @@ jobs: }); }; - const summary = ${{ toJSON(steps.summarize.outputs.summary) }}; - const llmPrompt = ${{ toJSON(steps.summarize.outputs.llm-prompt) }}; + const summary = process.env.SUMMARY; + const llmPrompt = process.env.LLM_PROMPT; const requestJournal = JSON.parse(await makeWireMockRequest('/__admin/requests')); const llmRequestEntry = requestJournal.requests.find(({ request }) => { return request.url === '/v1/chat/completions' || request.absoluteUrl?.includes('/v1/chat/completions'); diff --git a/.github/workflows/clean-deploy-argocd-app-of-apps.yml b/.github/workflows/clean-deploy-argocd-app-of-apps.yml index 0c00f7f..74cfa6c 100644 --- a/.github/workflows/clean-deploy-argocd-app-of-apps.yml +++ b/.github/workflows/clean-deploy-argocd-app-of-apps.yml @@ -53,28 +53,31 @@ jobs: steps: - id: check-client-payload uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + CLIENT_PAYLOAD: ${{ toJSON(github.event.client_payload) }} with: script: | - const environment = ${{ toJSON(github.event.client_payload.environment) }}; + const clientPayload = JSON.parse(process.env.CLIENT_PAYLOAD); + const environment = clientPayload.environment; if (!environment) { core.setFailed("Environment is not defined in the client payload"); return; } core.setOutput("environment", environment); - const repository = ${{ toJSON(github.event.client_payload.repository) }}; + const repository = clientPayload.repository; if (!repository) { core.setFailed("Repository is not defined in the client payload"); return; } core.setOutput("repository", repository); - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -86,19 +89,22 @@ jobs: - id: remove-files uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + APPLICATION_FILE: ${{ steps.get-manifest-files.outputs.application-file }} + MANIFEST_FILE: ${{ steps.get-manifest-files.outputs.manifest-file }} with: script: | const fs = require("node:fs"); let hasChanges = false; - const applicationFile = ${{ toJSON(steps.get-manifest-files.outputs.application-file) }}; + const applicationFile = process.env.APPLICATION_FILE; if (fs.existsSync(applicationFile)) { fs.unlinkSync(applicationFile); hasChanges = true; } - const manifestFile = ${{ toJSON(steps.get-manifest-files.outputs.manifest-file) }}; + const manifestFile = process.env.MANIFEST_FILE; if (fs.existsSync(manifestFile)) { fs.unlinkSync(manifestFile); hasChanges = true; @@ -112,13 +118,13 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional # jscpd:ignore-end - - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 if: steps.remove-files.outputs.has-changes == 'true' with: - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional branch: chore/clean-review-apps-${{ github.event.client_payload.repository }} title: "feat(${{ github.event.client_payload.repository }}): clean review apps" body: Clean review apps of ${{ github.event.client_payload.repository }} diff --git a/.github/workflows/clean-deploy.yml b/.github/workflows/clean-deploy.yml index 1c774e4..567509b 100644 --- a/.github/workflows/clean-deploy.yml +++ b/.github/workflows/clean-deploy.yml @@ -77,7 +77,6 @@ jobs: outputs: trigger: ${{ steps.trigger.outputs.trigger }} steps: - # jscpd:ignore-start - id: not-created-issue-comment if: github.event_name != 'issue_comment' run: echo "result=true" >> "$GITHUB_OUTPUT" @@ -94,17 +93,17 @@ jobs: - id: trigger if: ${{ steps.not-created-issue-comment.outputs.result == 'true' || steps.trigger-on-comment.outputs.triggered == 'true' }} - run: | - if [ "${{ steps.not-created-issue-comment.outputs.result }}" = "true" ]; then - echo "trigger=${{ github.event_name }}" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [ "${{ steps.trigger-on-comment.outputs.triggered }}" = "true" ]; then - echo "trigger=${{ github.event_name }}" >> "$GITHUB_OUTPUT" - exit 0 - fi - # jscpd:ignore-end + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + NOT_CREATED_ISSUE_COMMENT: ${{ steps.not-created-issue-comment.outputs.result }} + TRIGGERED: ${{ steps.trigger-on-comment.outputs.triggered }} + EVENT_NAME: ${{ github.event_name }} + with: + script: | + const shouldTrigger = process.env.NOT_CREATED_ISSUE_COMMENT === 'true' || process.env.TRIGGERED === 'true'; + if(shouldTrigger) { + core.setOutput("trigger", process.env.EVENT_NAME); + } clean-deploy: name: Clean deploy @@ -118,7 +117,7 @@ jobs: pull-requests: write steps: - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -143,22 +142,22 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional owner: ${{ steps.prepare-cleaning.outputs.owner }} - id: delete-deployment uses: ./../self-workflow/actions/deployment/delete with: - token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional - uses: ./../self-workflow/actions/clean-deploy/repository-dispatch if: ${{ steps.delete-deployment.outputs.environments && steps.delete-deployment.outputs.environments != '[]' && inputs.clean-deploy-type == 'repository-dispatch' }} with: repository: ${{ steps.prepare-cleaning.outputs.repository }} environment: ${{ fromJSON(steps.delete-deployment.outputs.environments)[0] }} - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional - - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 if: ${{ steps.delete-deployment.outputs.environments && steps.delete-deployment.outputs.environments != '[]' }} with: title: "Deployment(s) have been deleted :wastebasket:." diff --git a/.github/workflows/deploy-argocd-app-of-apps.md b/.github/workflows/deploy-argocd-app-of-apps.md index 4115bff..84a13de 100644 --- a/.github/workflows/deploy-argocd-app-of-apps.md +++ b/.github/workflows/deploy-argocd-app-of-apps.md @@ -115,7 +115,7 @@ jobs: -**ProTip:** Recommanded trigger event is `repository_dispatch`. +**ProTip:** Recommended trigger event is `repository_dispatch`. ```yaml name: "Deploy ArgoCD App of Apps" diff --git a/.github/workflows/deploy-argocd-app-of-apps.yml b/.github/workflows/deploy-argocd-app-of-apps.yml index bf63965..0635b91 100644 --- a/.github/workflows/deploy-argocd-app-of-apps.yml +++ b/.github/workflows/deploy-argocd-app-of-apps.yml @@ -93,68 +93,72 @@ jobs: steps: - id: check-client-payload uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CLIENT_PAYLOAD: ${{ toJSON(github.event.client_payload) }} with: script: | const fs = require("node:fs"); const path = require("node:path"); - const deploymentId = ${{ toJSON(github.event.client_payload['deployment-id']) }}; + const clientPayload = JSON.parse(process.env.INPUT_CLIENT_PAYLOAD); + + const deploymentId = clientPayload['deployment-id']; if (!deploymentId) { return core.setFailed("Deployment ID is not defined in the client payload"); } core.setOutput("deployment-id", deploymentId); - let environment = ${{ toJSON(github.event.client_payload.environment) }}; + let environment = clientPayload['environment']; if (!environment) { return core.setFailed("Environment is not defined in the client payload"); } core.setOutput("environment", environment); - const repository = ${{ toJSON(github.event.client_payload.repository) }}; + const repository = clientPayload['repository']; if (!repository) { return core.setFailed("Repository is not defined in the client payload"); } core.setOutput("repository", repository); - const url = ${{ toJSON(github.event.client_payload.url) }}; + const url = clientPayload['url']; if (url) { core.setOutput("url", url); } - const chart = ${{ toJSON(github.event.client_payload.chart) }}; + const chart = clientPayload['chart']; if (!chart) { return core.setFailed("Chart is not defined in the client payload"); } core.setOutput("chart", chart); - const chartValues = ${{ toJSON(github.event.client_payload['chart-values']) }}; + const chartValues = clientPayload['chart-values']; if (chartValues) { core.setOutput("chart-values", chartValues); } - const initiatedBy = ${{ toJSON(github.event.client_payload['initiated-by']) }}; + const initiatedBy = clientPayload['initiated-by']; if (!initiatedBy) { return core.setFailed("Initiated-by is not defined in the client payload"); } core.setOutput("initiated-by", initiatedBy); - id: chart-variables - uses: hoverkraft-tech/ci-github-container/actions/helm/parse-chart-uri@77f98ab8773b824eca7ed3f94e3e9c8b8af5875c # 0.36.1 + uses: hoverkraft-tech/ci-github-container/actions/helm/parse-chart-uri@5396e1258d209f9af18e55da8692361508e3338c # 0.36.2 with: uri: ${{ steps.check-client-payload.outputs.chart }} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - id: slugify-namespace - uses: hoverkraft-tech/ci-github-common/actions/slugify@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/slugify@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: value: ${{ format('{0}-{1}', steps.check-client-payload.outputs.repository, steps.check-client-payload.outputs.environment) }} # jscpd:ignore-start - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions # jscpd:ignore-end @@ -225,12 +229,12 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional # jscpd:ignore-end - - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional branch: feat/deploy-${{ steps.slugify-namespace.outputs.result }} title: "feat(${{ steps.check-client-payload.outputs.repository }}): deploy ${{ steps.chart-variables.outputs.version }} to ${{ steps.check-client-payload.outputs.environment }}" body: Deploy ${{ steps.check-client-payload.outputs.repository }} ${{ steps.chart-variables.outputs.version }} to ${{ steps.check-client-payload.outputs.environment }} diff --git a/.github/workflows/deploy-chart.md b/.github/workflows/deploy-chart.md index 9440661..6b47f3e 100644 --- a/.github/workflows/deploy-chart.md +++ b/.github/workflows/deploy-chart.md @@ -182,7 +182,7 @@ jobs: # Accept placeholders: # - `{{ tag }}`: will be replaced by the tag. # - `{{ url }}`: will be replaced by the URL. - # If "path" starts with "deploy", the chart value wil be passed to the deploy action. + # If "path" starts with "deploy", the chart value will be passed to the deploy action. # Example: # ```json # [ @@ -241,7 +241,7 @@ jobs: | | Accept placeholders: | | | | | | - `{{ tag }}`: will be replaced by the tag. | | | | | | - `{{ url }}`: will be replaced by the URL. | | | | -| | If "path" starts with "deploy", the chart value wil be passed to the deploy action. | | | | +| | If "path" starts with "deploy", the chart value will be passed to the deploy action. | | | | | | Example: | | | | | |
[
 { "path": ".image", "image": "application" },
 { "path": ".application.version", "value": "{{ tag }}" },
 { "path": "deploy.ingress.hosts[0].host", "value": "{{ url }}" }
]
| | | | | **`github-app-client-id`** | GitHub App Client ID to generate GitHub token in place of github-token. | **false** | **string** | - | diff --git a/.github/workflows/deploy-chart.yml b/.github/workflows/deploy-chart.yml index da017c8..4e412af 100644 --- a/.github/workflows/deploy-chart.yml +++ b/.github/workflows/deploy-chart.yml @@ -98,7 +98,7 @@ on: Accept placeholders: - `{{ tag }}`: will be replaced by the tag. - `{{ url }}`: will be replaced by the URL. - If "path" starts with "deploy", the chart value wil be passed to the deploy action. + If "path" starts with "deploy", the chart value will be passed to the deploy action. Example: ```json [ @@ -162,7 +162,7 @@ jobs: permissions: contents: read steps: - - uses: hoverkraft-tech/ci-github-common/actions/checkout@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/checkout@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 if: inputs.tag == '' with: fetch-depth: 0 @@ -175,7 +175,7 @@ jobs: - id: get-issue-number if: inputs.tag == '' && github.event_name == 'issue_comment' - uses: hoverkraft-tech/ci-github-common/actions/get-issue-number@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/get-issue-number@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 - id: get-tag uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -240,7 +240,7 @@ jobs: build-oci-images: name: Build OCI images needs: prepare-deploy - uses: hoverkraft-tech/ci-github-container/.github/workflows/docker-build-images.yml@77f98ab8773b824eca7ed3f94e3e9c8b8af5875c # 0.36.1 + uses: hoverkraft-tech/ci-github-container/.github/workflows/docker-build-images.yml@5396e1258d209f9af18e55da8692361508e3338c # 0.36.2 permissions: contents: read id-token: write @@ -354,14 +354,14 @@ jobs: core.setOutput('deploy-values', JSON.stringify(deployValues)); - id: release - uses: hoverkraft-tech/ci-github-container/actions/helm/release-chart@77f98ab8773b824eca7ed3f94e3e9c8b8af5875c # 0.36.1 + uses: hoverkraft-tech/ci-github-container/actions/helm/release-chart@5396e1258d209f9af18e55da8692361508e3338c # 0.36.2 with: chart: ${{ inputs.chart-name }} path: ${{ inputs.chart-path }} tag: ${{ needs.prepare-deploy.outputs.tag }} values: ${{ steps.set-chart-values.outputs.chart-values }} oci-registry: ${{ inputs.oci-registry }} - oci-registry-password: ${{ secrets.oci-registry-password }} + oci-registry-password: ${{ secrets.oci-registry-password }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional deploy-chart: name: Deploy chart @@ -455,12 +455,12 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional owner: ${{ steps.prepare-deployment.outputs.owner }} # jscpd:ignore-end - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -473,7 +473,7 @@ jobs: environment: ${{ needs.deploy-start.outputs.environment }} url: ${{ steps.prepare-deployment.outputs.url }} repository: ${{ steps.prepare-deployment.outputs.repository }} - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional initiated-by: ${{ github.actor }} deploy-finish: diff --git a/.github/workflows/deploy-checks.yml b/.github/workflows/deploy-checks.yml index 6945822..a779b28 100644 --- a/.github/workflows/deploy-checks.yml +++ b/.github/workflows/deploy-checks.yml @@ -71,7 +71,7 @@ jobs: summary: ${{ steps.generate-summary.outputs.summary }} steps: - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions diff --git a/.github/workflows/deploy-finish.yml b/.github/workflows/deploy-finish.yml index 5a8e0f1..1386e0f 100644 --- a/.github/workflows/deploy-finish.yml +++ b/.github/workflows/deploy-finish.yml @@ -56,7 +56,7 @@ jobs: environment: ${{ steps.get-finished-deployment.outputs.environment }} steps: - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -64,7 +64,7 @@ jobs: if: ${{ inputs.deployment-id }} uses: ./../self-workflow/actions/workflow/get-workflow-failure with: - github-token: ${{ secrets.github-token || github.token }} + github-token: ${{ secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional - id: get-finished-deployment if: ${{ inputs.deployment-id && steps.get-workflow-failure.outputs.has-failed != 'true' }} @@ -153,7 +153,7 @@ jobs: return core.setOutput("extra", JSON.stringify(extra)); - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -163,4 +163,4 @@ jobs: environment: ${{ needs.get-finished-deployment.outputs.environment }} url: ${{ needs.get-finished-deployment.outputs.url }} extra: ${{ steps.get-extra.outputs.extra }} - github-token: ${{ secrets.github-token || github.token }} + github-token: ${{ secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional diff --git a/.github/workflows/deploy-start.md b/.github/workflows/deploy-start.md index db6345d..4888115 100644 --- a/.github/workflows/deploy-start.md +++ b/.github/workflows/deploy-start.md @@ -44,7 +44,7 @@ Trigger: Environment: -- Support dynamic env when comming from issue or pull-request event +- Support dynamic env when coming from issue or pull-request event ### Permissions diff --git a/.github/workflows/deploy-start.yml b/.github/workflows/deploy-start.yml index cdb0738..eb6040b 100644 --- a/.github/workflows/deploy-start.yml +++ b/.github/workflows/deploy-start.yml @@ -14,7 +14,7 @@ # # Environment: # -# - Support dynamic env when comming from issue or pull-request event +# - Support dynamic env when coming from issue or pull-request event name: Deploy - Start @@ -97,7 +97,7 @@ jobs: } - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -121,7 +121,7 @@ jobs: deployment-id: ${{ steps.create-deployment.outputs.deployment-id }} steps: - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions diff --git a/.github/workflows/finish-deploy-argocd-app-of-apps.yml b/.github/workflows/finish-deploy-argocd-app-of-apps.yml index 91ab6ba..00516be 100644 --- a/.github/workflows/finish-deploy-argocd-app-of-apps.yml +++ b/.github/workflows/finish-deploy-argocd-app-of-apps.yml @@ -65,29 +65,34 @@ jobs: url: ${{ steps.check-client-payload.outputs.url }} state: ${{ steps.get-state-from-status.outputs.state }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - id: check-client-payload uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CLIENT_PAYLOAD: ${{ toJSON(github.event.client_payload) }} + with: script: | - core.debug(`Client payload: ${JSON.stringify(${{ toJSON(github.event.client_payload) }})}`); + core.debug(`Client payload: ${process.env.INPUT_CLIENT_PAYLOAD}`); + + const clientPayload = JSON.parse(process.env.INPUT_CLIENT_PAYLOAD); - const deploymentId = ${{ toJSON(github.event.client_payload['deployment-id']) }}; + const deploymentId = clientPayload['deployment-id']; if (!deploymentId) { return core.setFailed('"deployment-id" is not defined in the client payload'); } core.setOutput("deployment-id", deploymentId); - const repository = ${{ toJSON(github.event.client_payload['application-repository']) }}; + const repository = clientPayload['application-repository']; if (!repository) { return core.setFailed('"application-repository" is not defined in the client payload'); } core.setOutput("repository", repository); - let urls = ${{ toJSON(github.event.client_payload['urls']) }}; + let urls = clientPayload['urls']; if (urls) { if( !Array.isArray(urls)) { core.setFailed("URLs is not an array"); @@ -105,13 +110,13 @@ jobs: } } - const status = ${{ toJSON(github.event.client_payload['status']) }}; + const status = clientPayload['status']; if (!status) { return core.setFailed('"status" is not defined in the client payload'); } core.setOutput("status", status); - const description = ${{ toJSON(github.event.client_payload['description']) }}; + const description = clientPayload['description']; if (!description) { return core.setFailed('"description" is not defined in the client payload'); } @@ -119,9 +124,11 @@ jobs: - id: get-state-from-status uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + STATUS: ${{ steps.check-client-payload.outputs.status }} with: script: | - const status = ${{ toJSON(steps.check-client-payload.outputs.status) }}; + const status = process.env.STATUS; const statesStatuses = { "error": [], @@ -148,11 +155,11 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional owner: ${{ github.repository_owner }} - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -163,7 +170,7 @@ jobs: url: ${{ steps.check-client-payload.outputs.url }} description: ${{ steps.check-client-payload.outputs.description }} state: ${{ steps.get-state-from-status.outputs.state }} - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional update-log-url: "false" - uses: ./../self-workflow/actions/deploy/report @@ -171,7 +178,7 @@ jobs: with: deployment-id: ${{ github.event.client_payload['deployment-id'] }} repository: ${{ github.event.client_payload['application-repository'] }} - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional url: ${{ steps.check-client-payload.outputs.url }} extra: | { diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 0dba8f6..9f16e12 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -44,12 +44,12 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - id: local-workflow-actions - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: actions-path: actions @@ -61,6 +61,6 @@ jobs: - uses: release-drafter/release-drafter/autolabeler@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1 if: github.event_name == 'pull_request' with: - token: ${{ secrets.github-token || secrets.GITHUB_TOKEN || github.token }} + token: ${{ secrets.github-token || secrets.GITHUB_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional # config-path is relative to ".github" directory config-name: file:${{ steps.get-configuration.outputs.config-path }} diff --git a/.github/workflows/release-actions.yml b/.github/workflows/release-actions.yml index 00c2760..066f8d8 100644 --- a/.github/workflows/release-actions.yml +++ b/.github/workflows/release-actions.yml @@ -68,7 +68,7 @@ jobs: documentation-files: ${{ steps.get-changed-files.outputs.documentation-files }} all-documentation-files: ${{ steps.get-documentation-files.outputs.paths }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 @@ -267,7 +267,7 @@ jobs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} documentation-files: ${{ steps.generate-documentation.outputs.destination }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -308,7 +308,7 @@ jobs: if: needs.generate-documentation.outputs.artifact-id runs-on: ${{ fromJson(inputs.runs-on) }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -324,13 +324,13 @@ jobs: id: generate-token with: client-id: ${{ inputs.github-app-client-id }} - private-key: ${{ secrets.github-app-key }} + private-key: ${{ secrets.github-app-key }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional # jscpd:ignore-end - - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: - github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} + github-token: ${{ steps.generate-token.outputs.token || secrets.github-token || github.token }} # zizmor: ignore[secrets-outside-env] reusable workflow token override is intentional branch: docs/actions-workflows-documentation-update title: "docs: update actions and workflows documentation" body: Update actions and workflows documentation @@ -347,7 +347,7 @@ jobs: outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/Dockerfile b/Dockerfile index 162a729..e933e35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,6 @@ -FROM ghcr.io/super-linter/super-linter:slim-v8.0.0 +FROM ghcr.io/hoverkraft-tech/docker-base-images/super-linter:0.6.0 HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 CMD ["/bin/sh","-c","test -d /github/home"] ARG UID=1000 ARG GID=1000 -RUN chown -R ${UID}:${GID} /github/home -USER ${UID}:${GID} - -ENV RUN_LOCAL=true -ENV USE_FIND_ALGORITHM=true -ENV LOG_LEVEL=WARN -ENV LOG_FILE="/github/home/logs" +USER ${UID}:${GID} \ No newline at end of file diff --git a/Makefile b/Makefile index 6cbd9d9..4f2da22 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,11 @@ lint: ## Execute linting lint-fix: ## Execute linting and fix $(call run_linter, \ - -e FIX_JSON_PRETTIER=true \ - -e FIX_JAVASCRIPT_PRETTIER=true \ - -e FIX_YAML_PRETTIER=true \ -e FIX_MARKDOWN=true \ - -e FIX_MARKDOWN_PRETTIER=true \ -e FIX_NATURAL_LANGUAGE=true \ -e FIX_SHELL_SHFMT=true \ + -e FIX_BIOME_LINT=true \ + -e FIX_BIOME_FORMAT=true \ ) setup: ## Install npm dependencies for all package.json files under actions/ @@ -43,7 +41,6 @@ define run_linter docker run \ -e DEFAULT_WORKSPACE="$$DEFAULT_WORKSPACE" \ -e FILTER_REGEX_INCLUDE="$(filter-out $@,$(MAKECMDGOALS))" \ - -e IGNORE_GITIGNORED_FILES=true \ $(1) \ -v $$VOLUME \ --rm \ diff --git a/actions/argocd/get-manifest-files/action.yml b/actions/argocd/get-manifest-files/action.yml index 5c32705..ff0f338 100644 --- a/actions/argocd/get-manifest-files/action.yml +++ b/actions/argocd/get-manifest-files/action.yml @@ -29,9 +29,11 @@ runs: steps: - id: parse-environment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_ENVIRONMENT: ${{ inputs.environment }} with: script: | - let environment = ${{ toJSON(inputs.environment) }}; + const environment = process.env.INPUT_ENVIRONMENT; if (!environment) { return core.setFailed(`"environment" input is not defined`); } @@ -47,12 +49,15 @@ runs: - id: get-directories uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_ENVIRONMENT: ${{ steps.parse-environment.outputs.environment }} with: script: | const fs = require("node:fs"); const path = require("node:path"); - const environment = ${{ toJSON(steps.parse-environment.outputs.environment) }}; + const environment = process.env.INPUT_ENVIRONMENT; const globber = await glob.create(`./*/apps/${environment}/`,{ implicitDescendants: false, matchDirectories: true }); const paths = await globber.glob(); @@ -66,7 +71,7 @@ runs: const environmentDir = paths[0]; - const repository = ${{ toJSON(inputs.repository) }}; + const repository = process.env.INPUT_REPOSITORY; if (!repository) { return core.setFailed("Repository is not defined in the client payload"); } @@ -85,27 +90,32 @@ runs: return core.setFailed(`No manifest dir found in "${manifestDir}"`); } - core.setOutput("manifest-dir", path.relative(${{ toJSON(github.workspace) }}, manifestDir)); + core.setOutput("manifest-dir", path.relative(process.env.GITHUB_WORKSPACE, manifestDir)); - id: get-files uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_ENVIRONMENT_SUFFIX: ${{ steps.parse-environment.outputs.environment-suffix }} + INPUT_APPLICATION_DIR: ${{ steps.get-directories.outputs.application-dir }} + INPUT_MANIFEST_DIR: ${{ steps.get-directories.outputs.manifest-dir }} with: script: | const fs = require("node:fs"); const path = require("node:path"); - const repository = ${{ toJSON(inputs.repository) }}; - const environmentSuffix = ${{ toJSON(steps.parse-environment.outputs.environment-suffix) }}; - const applicationDir = ${{ toJSON(steps.get-directories.outputs.application-dir) }}; - const manifestDir = ${{ toJSON(steps.get-directories.outputs.manifest-dir) }}; + const repository = process.env.INPUT_REPOSITORY; + const environmentSuffix = process.env.INPUT_ENVIRONMENT_SUFFIX; + const applicationDir = process.env.INPUT_APPLICATION_DIR; + const manifestDir = process.env.INPUT_MANIFEST_DIR; // Templatable application if (environmentSuffix) { const applicationFile = `${applicationDir}/${environmentSuffix}.yml`; - core.setOutput("application-file", path.relative(${{ toJSON(github.workspace) }}, applicationFile)); + core.setOutput("application-file", path.relative(process.env.GITHUB_WORKSPACE, applicationFile)); const manifestFile = `${manifestDir}/${environmentSuffix}.yml`; - core.setOutput("manifest-file", path.relative(${{ toJSON(github.workspace) }}, manifestFile)); + core.setOutput("manifest-file", path.relative(process.env.GITHUB_WORKSPACE, manifestFile)); return; } @@ -114,10 +124,10 @@ runs: if (!fs.existsSync(applicationFile)) { return core.setFailed(`No application file found in "${applicationFile}"`); } - core.setOutput("application-file", path.relative(${{ toJSON(github.workspace) }}, applicationFile)); + core.setOutput("application-file", path.relative(process.env.GITHUB_WORKSPACE, applicationFile)); const manifestFile = `${manifestDir}/${repository}.yml`; if(!fs.existsSync(manifestFile)) { return core.setFailed(`No manifest file found in "${manifestFile}"`); } - core.setOutput("manifest-file", path.relative(${{ toJSON(github.workspace) }}, manifestFile)); + core.setOutput("manifest-file", path.relative(process.env.GITHUB_WORKSPACE, manifestFile)); diff --git a/actions/check/url-lighthouse/action.yml b/actions/check/url-lighthouse/action.yml index 788ba7f..f71b165 100644 --- a/actions/check/url-lighthouse/action.yml +++ b/actions/check/url-lighthouse/action.yml @@ -102,9 +102,12 @@ runs: - id: summary uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ARTIFACT_LINKS: ${{ steps.lighthouse.outputs.artifactLinks }} + MANIFEST: ${{ steps.lighthouse.outputs.manifest }} with: script: | - const linksOutput = ${{ toJSON(steps.lighthouse.outputs.artifactLinks) }}; + const linksOutput = process.env.ARTIFACT_LINKS?.trim(); let links = null; if (linksOutput) { // Check if is valid JSON @@ -121,7 +124,7 @@ runs: core.setOutput("report-url", reportUrl); } - const manifestOutput = ${{ toJSON(steps.lighthouse.outputs.manifest) }}; + const manifestOutput = process.env.MANIFEST?.trim(); let manifest = null; if (manifestOutput) { // Check if is valid JSON diff --git a/actions/check/url-ping/action.yml b/actions/check/url-ping/action.yml index e7db614..8f87526 100644 --- a/actions/check/url-ping/action.yml +++ b/actions/check/url-ping/action.yml @@ -45,6 +45,13 @@ runs: steps: - id: check-url uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_URL: ${{ inputs.url }} + INPUT_FOLLOW_REDIRECT: ${{ inputs['follow-redirect'] }} + INPUT_TIMEOUT: ${{ inputs.timeout }} + INPUT_RETRIES: ${{ inputs.retries }} + INPUT_EXPECTED_STATUSES: ${{ inputs['expected-statuses'] }} + INPUT_AUTHORIZATION: ${{ inputs.authorization }} with: script: | const path = require('path'); @@ -53,11 +60,11 @@ runs: await run({ core, inputs: { - url: ${{ toJson(inputs.url ) }}, - followRedirect: ${{ toJson(inputs['follow-redirect']) }}, - timeout: ${{ toJson(inputs.timeout) }}, - retries: ${{ toJson(inputs.retries) }}, - expectedStatuses: ${{ toJson(inputs['expected-statuses']) }}, - authorization: ${{ toJson(inputs.authorization) }}, + url: process.env.INPUT_URL, + followRedirect: process.env.INPUT_FOLLOW_REDIRECT, + timeout: process.env.INPUT_TIMEOUT, + retries: process.env.INPUT_RETRIES, + expectedStatuses: process.env.INPUT_EXPECTED_STATUSES, + authorization: process.env.INPUT_AUTHORIZATION, }, }); diff --git a/actions/check/url-ping/index.js b/actions/check/url-ping/index.js index 8189934..1121f30 100644 --- a/actions/check/url-ping/index.js +++ b/actions/check/url-ping/index.js @@ -1,403 +1,403 @@ const MS_IN_SECOND = 1000; const RETRY_POLICY = { - perAttemptTimeoutCapMs: 10_000, - baseBackoffMs: 300, - backoffFactor: 2, - maxBackoffMs: 15_000, - jitterRatio: 0.2, - minSleepMs: 50, - safetyMarginMs: 200, + perAttemptTimeoutCapMs: 10_000, + baseBackoffMs: 300, + backoffFactor: 2, + maxBackoffMs: 15_000, + jitterRatio: 0.2, + minSleepMs: 50, + safetyMarginMs: 200, }; const formatSeconds = (milliseconds) => - (milliseconds / MS_IN_SECOND).toFixed(2); + (milliseconds / MS_IN_SECOND).toFixed(2); const formatError = (error) => { - if (!error) { - return "Unknown error"; - } - - if (typeof error === "string") { - return error; - } - - const details = []; - const baseMessage = error.message || error.toString(); - if (baseMessage) { - details.push(baseMessage); - } - - const metaParts = []; - if (error.code) { - metaParts.push(`code=${error.code}`); - } - if (error.errno && error.errno !== error.code) { - metaParts.push(`errno=${error.errno}`); - } - if (error.syscall) { - metaParts.push(`syscall=${error.syscall}`); - } - if (error.hostname) { - metaParts.push(`hostname=${error.hostname}`); - } - if (metaParts.length) { - details.push(`[${metaParts.join(", ")}]`); - } - - if (error.cause) { - details.push(`cause: ${formatError(error.cause)}`); - } - - return details.join(" ").trim(); + if (!error) { + return "Unknown error"; + } + + if (typeof error === "string") { + return error; + } + + const details = []; + const baseMessage = error.message || error.toString(); + if (baseMessage) { + details.push(baseMessage); + } + + const metaParts = []; + if (error.code) { + metaParts.push(`code=${error.code}`); + } + if (error.errno && error.errno !== error.code) { + metaParts.push(`errno=${error.errno}`); + } + if (error.syscall) { + metaParts.push(`syscall=${error.syscall}`); + } + if (error.hostname) { + metaParts.push(`hostname=${error.hostname}`); + } + if (metaParts.length) { + details.push(`[${metaParts.join(", ")}]`); + } + + if (error.cause) { + details.push(`cause: ${formatError(error.cause)}`); + } + + return details.join(" ").trim(); }; const parsePositiveInteger = (rawValue, fieldName) => { - const parsed = parseInt(rawValue, 10); - if (Number.isNaN(parsed) || parsed <= 0) { - throw new Error( - `Invalid ${fieldName} input. Please provide a positive integer.`, - ); - } - return parsed; + const parsed = parseInt(rawValue, 10); + if (Number.isNaN(parsed) || parsed <= 0) { + throw new Error( + `Invalid ${fieldName} input, received '${rawValue}'. Please provide a positive integer.`, + ); + } + return parsed; }; const parseBooleanInput = (rawValue) => { - if (!rawValue) { - return false; - } - return rawValue.toString().toLowerCase() === "true"; + if (!rawValue) { + return false; + } + return rawValue.toString().toLowerCase() === "true"; }; const parseExpectedStatuses = (rawValue) => { - if (!rawValue) { - throw new Error("Expected statuses input is required."); - } + if (!rawValue) { + throw new Error("Expected statuses input is required."); + } - const statuses = rawValue - .split(",") - .map((status) => status.trim()) - .filter(Boolean); + const statuses = rawValue + .split(",") + .map((status) => status.trim()) + .filter(Boolean); - if (!statuses.length) { - throw new Error("Expected statuses input cannot be empty."); - } + if (!statuses.length) { + throw new Error("Expected statuses input cannot be empty."); + } - return statuses; + return statuses; }; const sanitizeOptionalString = (rawValue) => { - if (!rawValue) { - return null; - } - const trimmed = rawValue.toString().trim(); - return trimmed.length ? trimmed : null; + if (!rawValue) { + return null; + } + const trimmed = rawValue.toString().trim(); + return trimmed.length ? trimmed : null; }; const computeBackoffDelay = (attemptNumber, policy = RETRY_POLICY) => { - const growth = - policy.baseBackoffMs * Math.pow(policy.backoffFactor, attemptNumber - 1); - const capped = Math.min(policy.maxBackoffMs, growth); - const jitterSpan = capped * policy.jitterRatio; - const jitter = (Math.random() * 2 - 1) * jitterSpan; - return Math.max(policy.minSleepMs, Math.floor(capped + jitter)); + const growth = + policy.baseBackoffMs * policy.backoffFactor ** (attemptNumber - 1); + const capped = Math.min(policy.maxBackoffMs, growth); + const jitterSpan = capped * policy.jitterRatio; + const jitter = (Math.random() * 2 - 1) * jitterSpan; + return Math.max(policy.minSleepMs, Math.floor(capped + jitter)); }; const createDeadlineController = (deadlineMs) => { - const controller = new AbortController(); - const msLeft = deadlineMs - Date.now(); - - if (msLeft <= 0) { - controller.abort(new Error("Global deadline exceeded")); - return controller; - } - - const timer = setTimeout( - () => controller.abort(new Error("Global deadline exceeded")), - msLeft, - ); - controller._deadlineTimer = timer; - return controller; + const controller = new AbortController(); + const msLeft = deadlineMs - Date.now(); + + if (msLeft <= 0) { + controller.abort(new Error("Global deadline exceeded")); + return controller; + } + + const timer = setTimeout( + () => controller.abort(new Error("Global deadline exceeded")), + msLeft, + ); + controller._deadlineTimer = timer; + return controller; }; const clearDeadlineTimer = (controller) => { - if (controller && controller._deadlineTimer) { - clearTimeout(controller._deadlineTimer); - delete controller._deadlineTimer; - } + if (controller?._deadlineTimer) { + clearTimeout(controller._deadlineTimer); + delete controller._deadlineTimer; + } }; const buildRequestHeaders = (authorization) => { - const headers = { - "User-Agent": "hoverkraft-tech-url-ping-action", - }; + const headers = { + "User-Agent": "hoverkraft-tech-url-ping-action", + }; - if (authorization) { - headers.Authorization = authorization; - } + if (authorization) { + headers.Authorization = authorization; + } - return headers; + return headers; }; const fetchStatusCode = async ({ - core, - url, - followRedirect, - attemptTimeoutMs, - globalSignal, - authorization, + core, + url, + followRedirect, + attemptTimeoutMs, + globalSignal, + authorization, }) => { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), attemptTimeoutMs); - - const linkGlobalAbort = () => controller.abort(globalSignal.reason); - if (globalSignal) { - if (globalSignal.aborted) { - clearTimeout(timer); - throw globalSignal.reason || new Error("Global deadline exceeded"); - } - globalSignal.addEventListener("abort", linkGlobalAbort, { once: true }); - } - - try { - const fetchOptions = { - method: "HEAD", - redirect: followRedirect ? "follow" : "manual", - signal: controller.signal, - headers: buildRequestHeaders(authorization), - }; - core.debug( - `Fetching URL ${url.href} with options: ${JSON.stringify(fetchOptions)}`, - ); - - const response = await fetch(url, fetchOptions); - return response.status; - } catch (error) { - if (globalSignal && globalSignal.aborted) { - const reason = - globalSignal.reason instanceof Error - ? globalSignal.reason.message - : "Global deadline exceeded"; - throw new Error(reason); - } - if (error && error.name === "AbortError") { - throw new Error( - `Request to ${url} timed out after ${formatSeconds( - attemptTimeoutMs, - )} seconds (attempt budget)`, - ); - } - throw error; - } finally { - clearTimeout(timer); - if (globalSignal) { - globalSignal.removeEventListener("abort", linkGlobalAbort); - } - } + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), attemptTimeoutMs); + + const linkGlobalAbort = () => controller.abort(globalSignal.reason); + if (globalSignal) { + if (globalSignal.aborted) { + clearTimeout(timer); + throw globalSignal.reason || new Error("Global deadline exceeded"); + } + globalSignal.addEventListener("abort", linkGlobalAbort, { once: true }); + } + + try { + const fetchOptions = { + method: "HEAD", + redirect: followRedirect ? "follow" : "manual", + signal: controller.signal, + headers: buildRequestHeaders(authorization), + }; + core.debug( + `Fetching URL ${url.href} with options: ${JSON.stringify(fetchOptions)}`, + ); + + const response = await fetch(url, fetchOptions); + return response.status; + } catch (error) { + if (globalSignal?.aborted) { + const reason = + globalSignal.reason instanceof Error + ? globalSignal.reason.message + : "Global deadline exceeded"; + throw new Error(reason); + } + if (error && error.name === "AbortError") { + throw new Error( + `Request to ${url} timed out after ${formatSeconds( + attemptTimeoutMs, + )} seconds (attempt budget)`, + ); + } + throw error; + } finally { + clearTimeout(timer); + if (globalSignal) { + globalSignal.removeEventListener("abort", linkGlobalAbort); + } + } }; const ensureStatusIsExpected = (statusCode, expectedStatuses) => { - if (!expectedStatuses.includes(statusCode.toString())) { - throw new Error( - `Unexpected status code: ${statusCode}. Expected one of: ${expectedStatuses.join( - ", ", - )}`, - ); - } + if (!expectedStatuses.includes(statusCode.toString())) { + throw new Error( + `Unexpected status code: ${statusCode}. Expected one of: ${expectedStatuses.join( + ", ", + )}`, + ); + } }; const sleep = (milliseconds) => - new Promise((resolve) => setTimeout(resolve, milliseconds)); + new Promise((resolve) => setTimeout(resolve, milliseconds)); const logAttemptStart = ({ - core, - attemptNumber, - totalAttempts, - url, - attemptTimeoutMs, - remainingTimeMs, + core, + attemptNumber, + totalAttempts, + url, + attemptTimeoutMs, + remainingTimeMs, }) => { - core.info( - `Attempt ${attemptNumber}/${totalAttempts} - Checking URL: ${url} ` + - `(attempt timeout: ${formatSeconds( - attemptTimeoutMs, - )}s, remaining total time: ${formatSeconds(remainingTimeMs)}s)`, - ); + core.info( + `Attempt ${attemptNumber}/${totalAttempts} - Checking URL: ${url} ` + + `(attempt timeout: ${formatSeconds( + attemptTimeoutMs, + )}s, remaining total time: ${formatSeconds(remainingTimeMs)}s)`, + ); }; const executeAttempt = async ({ - config, - attemptTimeoutMs, - core, - globalSignal, + config, + attemptTimeoutMs, + core, + globalSignal, }) => { - const statusCode = await fetchStatusCode({ - core, - url: config.url, - followRedirect: config.followRedirect, - attemptTimeoutMs, - globalSignal, - authorization: config.authorization, - }); - core.setOutput("status-code", statusCode); - ensureStatusIsExpected(statusCode, config.expectedStatuses); - return statusCode; + const statusCode = await fetchStatusCode({ + core, + url: config.url, + followRedirect: config.followRedirect, + attemptTimeoutMs, + globalSignal, + authorization: config.authorization, + }); + core.setOutput("status-code", statusCode); + ensureStatusIsExpected(statusCode, config.expectedStatuses); + return statusCode; }; const createConfig = (rawInputs) => { - const urlInput = rawInputs.url; - if (!urlInput) { - throw new Error("URL input is required."); - } - - const url = new URL(urlInput.trim()); - if (!url.protocol || !url.host) { - throw new Error("Invalid URL input. Please provide a valid URL."); - } - - const followRedirect = parseBooleanInput(rawInputs.followRedirect); - const timeoutSeconds = parsePositiveInteger(rawInputs.timeout, "timeout"); - const totalTimeoutMs = timeoutSeconds * MS_IN_SECOND; - const totalAttempts = parsePositiveInteger(rawInputs.retries, "retries"); - const expectedStatuses = parseExpectedStatuses(rawInputs.expectedStatuses); - const authorization = sanitizeOptionalString(rawInputs.authorization); - - return { - url, - followRedirect, - timeoutSeconds, - totalTimeoutMs, - totalAttempts, - expectedStatuses, - authorization, - }; + const urlInput = rawInputs.url; + if (!urlInput) { + throw new Error("URL input is required."); + } + + const url = new URL(urlInput.trim()); + if (!url.protocol || !url.host) { + throw new Error("Invalid URL input. Please provide a valid URL."); + } + + const followRedirect = parseBooleanInput(rawInputs.followRedirect); + const timeoutSeconds = parsePositiveInteger(rawInputs.timeout, "timeout"); + const totalTimeoutMs = timeoutSeconds * MS_IN_SECOND; + const totalAttempts = parsePositiveInteger(rawInputs.retries, "retries"); + const expectedStatuses = parseExpectedStatuses(rawInputs.expectedStatuses); + const authorization = sanitizeOptionalString(rawInputs.authorization); + + return { + url, + followRedirect, + timeoutSeconds, + totalTimeoutMs, + totalAttempts, + expectedStatuses, + authorization, + }; }; const runUrlCheck = async (config, core) => { - core.debug(`Configuration: ${JSON.stringify(config)}`); - - const startTime = Date.now(); - const deadline = startTime + config.totalTimeoutMs; - const globalController = createDeadlineController(deadline); - const maxAttempts = config.totalAttempts; - const slotLength = Math.max( - RETRY_POLICY.minSleepMs, - Math.floor(config.totalTimeoutMs / maxAttempts), - ); - - let lastError = null; - - try { - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - if (deadline - Date.now() <= 0) { - throw new Error( - `URL check timed out after ${config.timeoutSeconds} seconds before attempt ${attempt} could start.`, - ); - } - - const slotStart = startTime + (attempt - 1) * slotLength; - const slotEndExclusive = - attempt < maxAttempts ? startTime + attempt * slotLength : deadline; - - const preSleepMs = slotStart - Date.now(); - if (preSleepMs > 0) { - await sleep(preSleepMs); - } - - const remainingGlobalMs = Math.max(0, deadline - Date.now()); - const remainingSlotMs = Math.max(0, slotEndExclusive - Date.now()); - const attemptTimeoutMs = Math.max( - RETRY_POLICY.minSleepMs, - Math.min( - RETRY_POLICY.perAttemptTimeoutCapMs, - Math.max(0, remainingSlotMs - RETRY_POLICY.safetyMarginMs), - ), - ); - - logAttemptStart({ - core, - attemptNumber: attempt, - totalAttempts: maxAttempts, - url: config.url, - attemptTimeoutMs, - remainingTimeMs: remainingGlobalMs, - }); - - try { - const statusCode = await executeAttempt({ - config, - attemptTimeoutMs, - core, - globalSignal: globalController.signal, - }); - core.setOutput("attempt-count", attempt); - core.info( - `URL check succeeded with status code: ${statusCode} after ${attempt} attempt(s).`, - ); - return; - } catch (error) { - core.setOutput("attempt-count", attempt); - const failure = - error instanceof Error ? error : new Error(String(error)); - lastError = failure; - - core.warning( - `Attempt ${attempt}/${maxAttempts} failed: ${formatError(failure)}`, - ); - if (failure.stack) { - core.debug(failure.stack); - } - - if (failure.message.includes("Global deadline exceeded")) { - throw new Error( - `URL check timed out after ${ - config.timeoutSeconds - } seconds. Last error: ${formatError(failure)}`, - ); - } - - if (attempt === maxAttempts) { - break; - } - - const backoffDelay = computeBackoffDelay(attempt); - core.info( - `Retrying in ${backoffDelay}ms (budget-aware exponential backoff).`, - ); - await sleep(backoffDelay); - } - } - - const remainingToDeadline = deadline - Date.now(); - if (remainingToDeadline > 0) { - core.info( - `Failure detected; waiting until global deadline for ${remainingToDeadline}ms.`, - ); - await sleep(remainingToDeadline); - } - - const failureSummary = `URL check failed after ${maxAttempts} attempt(s) within ${config.timeoutSeconds} seconds.`; - if (lastError) { - throw new Error( - `${failureSummary} Last error: ${formatError(lastError)}`, - ); - } - throw new Error(failureSummary); - } finally { - clearDeadlineTimer(globalController); - } + core.debug(`Configuration: ${JSON.stringify(config)}`); + + const startTime = Date.now(); + const deadline = startTime + config.totalTimeoutMs; + const globalController = createDeadlineController(deadline); + const maxAttempts = config.totalAttempts; + const slotLength = Math.max( + RETRY_POLICY.minSleepMs, + Math.floor(config.totalTimeoutMs / maxAttempts), + ); + + let lastError = null; + + try { + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + if (deadline - Date.now() <= 0) { + throw new Error( + `URL check timed out after ${config.timeoutSeconds} seconds before attempt ${attempt} could start.`, + ); + } + + const slotStart = startTime + (attempt - 1) * slotLength; + const slotEndExclusive = + attempt < maxAttempts ? startTime + attempt * slotLength : deadline; + + const preSleepMs = slotStart - Date.now(); + if (preSleepMs > 0) { + await sleep(preSleepMs); + } + + const remainingGlobalMs = Math.max(0, deadline - Date.now()); + const remainingSlotMs = Math.max(0, slotEndExclusive - Date.now()); + const attemptTimeoutMs = Math.max( + RETRY_POLICY.minSleepMs, + Math.min( + RETRY_POLICY.perAttemptTimeoutCapMs, + Math.max(0, remainingSlotMs - RETRY_POLICY.safetyMarginMs), + ), + ); + + logAttemptStart({ + core, + attemptNumber: attempt, + totalAttempts: maxAttempts, + url: config.url, + attemptTimeoutMs, + remainingTimeMs: remainingGlobalMs, + }); + + try { + const statusCode = await executeAttempt({ + config, + attemptTimeoutMs, + core, + globalSignal: globalController.signal, + }); + core.setOutput("attempt-count", attempt); + core.info( + `URL check succeeded with status code: ${statusCode} after ${attempt} attempt(s).`, + ); + return; + } catch (error) { + core.setOutput("attempt-count", attempt); + const failure = + error instanceof Error ? error : new Error(String(error)); + lastError = failure; + + core.warning( + `Attempt ${attempt}/${maxAttempts} failed: ${formatError(failure)}`, + ); + if (failure.stack) { + core.debug(failure.stack); + } + + if (failure.message.includes("Global deadline exceeded")) { + throw new Error( + `URL check timed out after ${ + config.timeoutSeconds + } seconds. Last error: ${formatError(failure)}`, + ); + } + + if (attempt === maxAttempts) { + break; + } + + const backoffDelay = computeBackoffDelay(attempt); + core.info( + `Retrying in ${backoffDelay}ms (budget-aware exponential backoff).`, + ); + await sleep(backoffDelay); + } + } + + const remainingToDeadline = deadline - Date.now(); + if (remainingToDeadline > 0) { + core.info( + `Failure detected; waiting until global deadline for ${remainingToDeadline}ms.`, + ); + await sleep(remainingToDeadline); + } + + const failureSummary = `URL check failed after ${maxAttempts} attempt(s) within ${config.timeoutSeconds} seconds.`; + if (lastError) { + throw new Error( + `${failureSummary} Last error: ${formatError(lastError)}`, + ); + } + throw new Error(failureSummary); + } finally { + clearDeadlineTimer(globalController); + } }; const run = async ({ core, inputs }) => { - try { - const config = createConfig(inputs); - await runUrlCheck(config, core); - } catch (error) { - const message = error instanceof Error ? formatError(error) : String(error); - core.setFailed(message); - } + try { + const config = createConfig(inputs); + await runUrlCheck(config, core); + } catch (error) { + const message = error instanceof Error ? formatError(error) : String(error); + core.setFailed(message); + } }; module.exports = { - run, + run, }; diff --git a/actions/deploy/argocd-manifest-files/action.yml b/actions/deploy/argocd-manifest-files/action.yml index 60fd1f7..b1186f9 100644 --- a/actions/deploy/argocd-manifest-files/action.yml +++ b/actions/deploy/argocd-manifest-files/action.yml @@ -168,9 +168,12 @@ runs: steps: - id: common-chart-yq-updates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CHART_VALUES: ${{ inputs.chart-values }} + INPUT_DEPLOYMENT_ID: ${{ inputs.deployment-id }} with: script: | - const chartValuesInput = ${{ toJSON(inputs.chart-values) }}; + const chartValuesInput = process.env.INPUT_CHART_VALUES.trim(); let chartValues = null; try { chartValues = JSON.parse(chartValuesInput); @@ -198,7 +201,7 @@ runs: // Add deployment ID to each chart value chartValues.push({ path: ".deploymentId", - value: "${{ inputs.deployment-id }}" + value: process.env.INPUT_DEPLOYMENT_ID }); const yqUpdates = chartValues.map(chartValue => `${chartValue.path} = "${chartValue.value}" |`).join("\n"); @@ -207,107 +210,122 @@ runs: - id: vendor-specific-chart-yq-updates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CHART_VERSION: ${{ inputs.chart-version }} with: script: | // Datadog - const yqUpdates = `(.. | select(has("tags.datadoghq.com/version")))."tags.datadoghq.com/version" = "${{ inputs.chart-version }}" |`; + const yqUpdates = `(.. | select(has("tags.datadoghq.com/version")))."tags.datadoghq.com/version" = "${process.env.INPUT_CHART_VERSION}" |`; core.setOutput('cmd', yqUpdates); - id: update-application-file uses: mikefarah/yq@751d8ad57b84f1794661bc70c0afb92a22ad7b3c # v4.53.2 + env: + INPUT_NAMESPACE: ${{ inputs.namespace }} + INPUT_APPLICATION_REPOSITORY: ${{ inputs.application-repository }} + INPUT_APPLICATION_FILE: ${{ inputs.application-file }} + INPUT_DEPLOYMENT_ID: ${{ inputs.deployment-id }} + INPUT_CHART_NAME: ${{ inputs.chart-name }} + INPUT_CHART_REPOSITORY: ${{ inputs.chart-repository }} + INPUT_CHART_VERSION: ${{ inputs.chart-version }} + INPUT_COMMON_CHART_YQ_UPDATES: ${{ steps.common-chart-yq-updates.outputs.cmd }} + INPUT_VENDOR_SPECIFIC_CHART_YQ_UPDATES: ${{ steps.vendor-specific-chart-yq-updates.outputs.cmd }} with: cmd: | # Update ArgoCD Application manifest file yq -i ' - .metadata.name = "${{ inputs.namespace }}" | - .metadata.annotations["argocd.argoproj.io/application-repository"] = "${{ inputs.application-repository }}" | - .metadata.annotations["argocd.argoproj.io/deployment-id"] = "${{ inputs.deployment-id }}" | - .spec.destination.namespace = "${{ inputs.namespace }}" - ' ${{ inputs.application-file }} + .metadata.name = strenv(INPUT_NAMESPACE) | + .metadata.annotations["argocd.argoproj.io/application-repository"] = strenv(INPUT_APPLICATION_REPOSITORY) | + .metadata.annotations["argocd.argoproj.io/deployment-id"] = strenv(INPUT_DEPLOYMENT_ID) | + .spec.destination.namespace = strenv(INPUT_NAMESPACE) + ' "$INPUT_APPLICATION_FILE" # Detect whether the manifest uses singular 'source' or 'sources' array - if [ "$(yq eval '.spec | has("source")' ${{ inputs.application-file }})" = "true" ]; then + if [ "$(yq eval '.spec | has("source")' "$INPUT_APPLICATION_FILE" = "true" ]; then # Singular source format - current_chart=$(yq eval -r '.spec.source.chart // ""' ${{ inputs.application-file }}) - if [ "$current_chart" != "${{ inputs.chart-name }}" ]; then - echo "::error::ArgoCD manifest chart ('$current_chart') does not match input chart-name ('${{ inputs.chart-name }}')" + current_chart=$(yq eval -r '.spec.source.chart // ""' "$INPUT_APPLICATION_FILE") + if [ "$current_chart" != "$INPUT_CHART_NAME" ]; then + echo "::error::ArgoCD manifest chart ('$current_chart') does not match input chart-name ('$INPUT_CHART_NAME')" exit 1 fi - current_repo=$(yq eval -r '.spec.source.repoURL // ""' ${{ inputs.application-file }}) - if [ "$current_repo" != "${{ inputs.chart-repository }}" ]; then - echo "::error::ArgoCD manifest repoURL ('$current_repo') does not match input chart-repository ('${{ inputs.chart-repository }}')" + current_repo=$(yq eval -r '.spec.source.repoURL // ""' "$INPUT_APPLICATION_FILE") + if [ "$current_repo" != "$INPUT_CHART_REPOSITORY" ]; then + echo "::error::ArgoCD manifest repoURL ('$current_repo') does not match input chart-repository ('$INPUT_CHART_REPOSITORY')" exit 1 fi yq -i ' - .spec.source.targetRevision = "${{ inputs.chart-version }}" | + .spec.source.targetRevision = strenv(INPUT_CHART_VERSION) | .spec.source.helm.values |= ( from_yaml | - ${{ steps.common-chart-yq-updates.outputs.cmd }} - ${{ steps.vendor-specific-chart-yq-updates.outputs.cmd }} + strenv(INPUT_COMMON_CHART_YQ_UPDATES) + strenv(INPUT_VENDOR_SPECIFIC_CHART_YQ_UPDATES) to_yaml ) - ' ${{ inputs.application-file }} + ' "$INPUT_APPLICATION_FILE" # Update plugin env if it exists on source and plugin name is "hoverkraft-deployment" - if [ "$(yq eval '.spec.source.plugin.name' ${{ inputs.application-file }})" = "hoverkraft-deployment" ]; then + if [ "$(yq eval '.spec.source.plugin.name' "$INPUT_APPLICATION_FILE")" = "hoverkraft-deployment" ]; then yq -i ' - (.spec.source.plugin.env[] | select(.name == "HOVERKRAFT_DEPLOYMENT_ID") | .value) = "${{ inputs.deployment-id }}" | + (.spec.source.plugin.env[] | select(.name == "HOVERKRAFT_DEPLOYMENT_ID") | .value) = strenv(INPUT_DEPLOYMENT_ID) | (.spec.source.plugin.env[] | select(.name == "ARGOCD_MULTI_SOURCES") | .value) = "0" - ' ${{ inputs.application-file }} + ' "$INPUT_APPLICATION_FILE" fi else # Multiple sources array format matching_source_count=$(yq eval ' (.spec.sources // []) - | map(select(.chart == "${{ inputs.chart-name }}" and .repoURL == "${{ inputs.chart-repository }}")) + | map(select(.chart == strenv(INPUT_CHART_NAME) and .repoURL == strenv(INPUT_CHART_REPOSITORY))) | length - ' ${{ inputs.application-file }}) + ' "$INPUT_APPLICATION_FILE") if [ "$matching_source_count" -eq 0 ]; then - echo "::error::No entry in spec.sources matches chart '${{ inputs.chart-name }}' and repoURL '${{ inputs.chart-repository }}'" + echo "::error::No entry in spec.sources matches chart '$INPUT_CHART_NAME' and repoURL '$INPUT_CHART_REPOSITORY'" exit 1 fi yq -i ' - (.spec.sources[] | select(.chart == "${{ inputs.chart-name }}" and .repoURL == "${{ inputs.chart-repository }}") | .targetRevision) = "${{ inputs.chart-version }}" | - (.spec.sources[] | select(.chart == "${{ inputs.chart-name }}" and .repoURL == "${{ inputs.chart-repository }}" and has("helm")) | .helm.values) |= ( + (.spec.sources[] | select(.chart == strenv(INPUT_CHART_NAME) and .repoURL == strenv(INPUT_CHART_REPOSITORY)) | .targetRevision) = strenv(INPUT_CHART_VERSION) | + (.spec.sources[] | select(.chart == strenv(INPUT_CHART_NAME) and .repoURL == strenv(INPUT_CHART_REPOSITORY) and has("helm")) | .helm.values) |= ( from_yaml | - ${{ steps.common-chart-yq-updates.outputs.cmd }} - ${{ steps.vendor-specific-chart-yq-updates.outputs.cmd }} + strenv(INPUT_COMMON_CHART_YQ_UPDATES) + strenv(INPUT_VENDOR_SPECIFIC_CHART_YQ_UPDATES) to_yaml ) - ' ${{ inputs.application-file }} + ' "$INPUT_APPLICATION_FILE" # Update plugin env only for matching sources where plugin name is "hoverkraft-deployment" yq -i ' ( .spec.sources[] - | select(.chart == "${{ inputs.chart-name }}" and .repoURL == "${{ inputs.chart-repository }}") + | select(.chart == strenv(INPUT_CHART_NAME) and .repoURL == strenv(INPUT_CHART_REPOSITORY)) | select(.plugin.name == "hoverkraft-deployment") | .plugin.env[] | select(.name == "HOVERKRAFT_DEPLOYMENT_ID") | .value - ) = "${{ inputs.deployment-id }}" | + ) = strenv(INPUT_DEPLOYMENT_ID) | ( .spec.sources[] - | select(.chart == "${{ inputs.chart-name }}" and .repoURL == "${{ inputs.chart-repository }}") + | select(.chart == strenv(INPUT_CHART_NAME) and .repoURL == strenv(INPUT_CHART_REPOSITORY)) | select(.plugin.name == "hoverkraft-deployment") | .plugin.env[] | select(.name == "ARGOCD_MULTI_SOURCES") | .value ) = "1" - ' ${{ inputs.application-file }} + ' "$INPUT_APPLICATION_FILE" fi - id: update-manifest-file uses: mikefarah/yq@751d8ad57b84f1794661bc70c0afb92a22ad7b3c # v4.53.2 + env: + INPUT_NAMESPACE: ${{ inputs.namespace }} + INPUT_MANIFEST_FILE: ${{ inputs.manifest-file }} with: cmd: | yq -i ' - (select(has("metadata") and .metadata | has("name")) | .metadata.name) = "${{ inputs.namespace }}" | - (select(has("metadata") and .metadata | has("namespace")) | .metadata.namespace) = "${{ inputs.namespace }}" | - (select(has("metadata") and .metadata | has("annotations") and .metadata.annotations | has("app.kubernetes.io/instance")) | .metadata.annotations["app.kubernetes.io/instance"]) = "${{ inputs.namespace }}" - ' ${{ inputs.manifest-file }} + (select(has("metadata") and .metadata | has("name")) | .metadata.name) = strenv(INPUT_NAMESPACE) | + (select(has("metadata") and .metadata | has("namespace")) | .metadata.namespace) = strenv(INPUT_NAMESPACE) | + (select(has("metadata") and .metadata | has("annotations") and .metadata.annotations | has("app.kubernetes.io/instance")) | .metadata.annotations["app.kubernetes.io/instance"]) = strenv(INPUT_NAMESPACE) + ' "$INPUT_MANIFEST_FILE" diff --git a/actions/deploy/get-environment/README.md b/actions/deploy/get-environment/README.md index deaeda6..e6c0013 100644 --- a/actions/deploy/get-environment/README.md +++ b/actions/deploy/get-environment/README.md @@ -31,7 +31,7 @@ Action to get the environment to deploy regarding the workflow context. - If the workflow is triggered by an issue event (or pull-request): - If an environement is given, the environment will be set to `environment:issue_number`. + If an environment is given, the environment will be set to `environment:issue_number`. If no environment is given, the environment will be set to `review-apps:issue_number`. - Else if no environment is given, the action will fail. diff --git a/actions/deploy/get-environment/action.yml b/actions/deploy/get-environment/action.yml index 9865f7d..dbe0f6c 100644 --- a/actions/deploy/get-environment/action.yml +++ b/actions/deploy/get-environment/action.yml @@ -7,8 +7,8 @@ description: | Action to get the environment to deploy regarding the workflow context. - If the workflow is triggered by an issue event (or pull-request): - If an environement is given, the environment will be set to `environment:issue_number`. - If no environment is given, the environment will be set to `review-apps:issue_number`. + If an environment is given, the environment will be set to `:pr-`. + If no environment is given, the environment will be set to `review-apps:pr-`. - Else if no environment is given, the action will fail. inputs: @@ -26,14 +26,17 @@ runs: steps: - id: get-issue-number if: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request' }} - uses: hoverkraft-tech/ci-github-common/actions/get-issue-number@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/get-issue-number@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 - id: get-environment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_ENVIRONMENT: ${{ inputs.environment }} + STEP_ISSUE_NUMBER: ${{ steps.get-issue-number.outputs.issue-number }} with: script: | - let environment = ${{ toJSON(inputs.environment) }}; - const issueNumber = ${{ toJSON(steps.get-issue-number.outputs.issue-number) }}; + let environment = process.env.INPUT_ENVIRONMENT; + const issueNumber = process.env.STEP_ISSUE_NUMBER; if (issueNumber) { if (!environment) { diff --git a/actions/deploy/github-pages/action.yml b/actions/deploy/github-pages/action.yml index 106f3e6..e0ec915 100644 --- a/actions/deploy/github-pages/action.yml +++ b/actions/deploy/github-pages/action.yml @@ -59,18 +59,22 @@ runs: - id: prepare-variables uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + DOWNLOAD_PATH: ${{ steps.download-artifact.outputs.download-path }} + INPUT_BUILD_PATH: ${{ inputs.build-path }} + with: script: | const { isAbsolute, join } = require("path"); const { lstatSync } = require("fs"); const { randomUUID } = require('crypto'); - let downloadPath = ${{ toJSON(steps.download-artifact.outputs.download-path) }}; + let downloadPath = process.env.DOWNLOAD_PATH; if (!downloadPath || downloadPath === "/") { - downloadPath = ${{ toJSON(github.workspace) }}; + downloadPath = process.env.GITHUB_WORKSPACE; } - let buildPath = ${{ toJSON(inputs.build-path) }}; + let buildPath = process.env.INPUT_BUILD_PATH; if (!buildPath || !isAbsolute(buildPath)) { buildPath = join(downloadPath.trim(), buildPath.trim()); } @@ -85,11 +89,13 @@ runs: // Define a unique artifact name const uniquid = randomUUID(); const timestamp = Date.now(); - const artifactName = `${{ github.run_id }}-${{ github.run_number }}-github-pages-${timestamp}-${uniquid}`; + const runId = process.env.GITHUB_RUN_ID; + const runNumber = process.env.GITHUB_RUN_NUMBER; + const artifactName = `${runId}-${runNumber}-github-pages-${timestamp}-${uniquid}`; core.setOutput("artifact-name", artifactName); - id: local-actions - uses: hoverkraft-tech/ci-github-common/actions/local-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: source-path: ${{ github.action_path }}/../.. diff --git a/actions/deploy/helm-repository-dispatch/action.yml b/actions/deploy/helm-repository-dispatch/action.yml index 348d029..3745066 100644 --- a/actions/deploy/helm-repository-dispatch/action.yml +++ b/actions/deploy/helm-repository-dispatch/action.yml @@ -59,9 +59,11 @@ runs: steps: - id: set-chart-values uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_CHART_VALUES: ${{ inputs.chart-values }} with: script: | - const chartValuesInput = `${{ inputs.chart-values }}`.trim(); + const chartValuesInput = process.env.INPUT_CHART_VALUES.trim(); if (!chartValuesInput.length) { core.setOutput("chart-values", JSON.stringify([])); diff --git a/actions/deploy/jampack/action.yml b/actions/deploy/jampack/action.yml index 2d6fdd0..9b18503 100644 --- a/actions/deploy/jampack/action.yml +++ b/actions/deploy/jampack/action.yml @@ -41,12 +41,15 @@ runs: ${{ runner.os }}-jampack- - shell: bash + env: + JAMPACK_CACHE_FOLDER: "${{ runner.temp }}/.jampack/cache" + INPUT_PATH: ${{ inputs.path }} run: | # Create a cache directory for Jampack - mkdir -p "${{ runner.temp }}/.jampack/cache" + mkdir -p "${JAMPACK_CACHE_FOLDER}" # Run Jampack on the specified path - npx @divriots/jampack --cache_folder "${{ runner.temp }}/.jampack/cache" "${{ inputs.path }}" + npx @divriots/jampack --cache_folder "${JAMPACK_CACHE_FOLDER}" "${INPUT_PATH}" # Clean up the Jampack cache directory - rm -rf "${{ inputs.path }}/_jampack" + rm -rf "${INPUT_PATH}/_jampack" diff --git a/actions/deploy/jekyll/action.yml b/actions/deploy/jekyll/action.yml index 7f4fec8..0faa50f 100644 --- a/actions/deploy/jekyll/action.yml +++ b/actions/deploy/jekyll/action.yml @@ -67,6 +67,12 @@ runs: steps: - id: prepare-site uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_THEME: ${{ inputs.theme }} + INPUT_PAGES: ${{ inputs.pages }} + INPUT_ASSETS: ${{ inputs.assets }} + INPUT_SITE_PATH: ${{ inputs.site-path }} + INPUT_BUILD_PATH: ${{ inputs.build-path }} with: script: | const path = require('path'); @@ -76,11 +82,11 @@ runs: io, glob, inputs: { - theme: ${{ toJson(inputs.theme) }}, - pages: ${{ toJson(inputs.pages) }}, - assets: ${{ toJson(inputs.assets) }}, - "site-path": ${{ toJson(inputs.site-path) }}, - "build-path": ${{ toJson(inputs.build-path) }} + theme: process.env.INPUT_THEME, + pages: process.env.INPUT_PAGES, + assets: process.env.INPUT_ASSETS, + "site-path": process.env.INPUT_SITE_PATH, + "build-path": process.env.INPUT_BUILD_PATH } }); @@ -93,4 +99,6 @@ runs: destination: ${{ steps.prepare-site.outputs.jekyll-destination }} - shell: bash - run: sudo chown -R $(whoami) "${{ steps.prepare-site.outputs.jekyll-destination }}" + env: + JEKYLL_DESTINATION: ${{ steps.prepare-site.outputs.jekyll-destination }} + run: sudo chown -R $(whoami) "${JEKYLL_DESTINATION}" diff --git a/actions/deploy/jekyll/asset-manager.js b/actions/deploy/jekyll/asset-manager.js index de1ec93..9f1cb2d 100644 --- a/actions/deploy/jekyll/asset-manager.js +++ b/actions/deploy/jekyll/asset-manager.js @@ -1,6 +1,6 @@ -const { randomUUID } = require("crypto"); -const { join, basename, dirname, resolve, sep } = require("path"); -const { statSync, mkdirSync } = require("fs"); +const { randomUUID } = require("node:crypto"); +const { join, basename, dirname, resolve, sep } = require("node:path"); +const { statSync, mkdirSync } = require("node:fs"); const { WorkspacePathResolver } = require("./workspace-path-resolver"); const { SiteFileManager } = require("./site-file-manager"); @@ -11,223 +11,223 @@ const HTML_IMAGE_REGEX = /]+src=["']([^"']+)["'][^>]*>/gi; const HTML_SOURCE_REGEX = /]+srcset=["']([^"']+)["'][^>]*>/gi; class AssetManager { - constructor({ workspacePath, sitePath }) { - this.workspacePath = workspacePath; - this.sitePath = sitePath; - this.cache = new Map(); - this.workspacePathResolver = new WorkspacePathResolver({ workspacePath }); - this.siteFileManager = new SiteFileManager(); - } - - copyAssetFromWorkspace(assetAbsolutePath) { - if (!assetAbsolutePath) { - return null; - } - - const assetPathInfo = this.#resolveAssetPathInfo(assetAbsolutePath); - if (!assetPathInfo) { - return null; - } - - const stats = statSync(assetPathInfo.path); - if (!stats.isFile()) { - return null; - } - - let record = this.cache.get(assetPathInfo.path); - if (!record) { - const sanitizedTarget = this.#sanitizeAssetTarget( - assetPathInfo.relativePath, - assetPathInfo.path, - ); - const destination = join(this.sitePath, ASSETS_ROOT, sanitizedTarget); - mkdirSync(dirname(destination), { recursive: true }); - this.siteFileManager.copyFile(assetPathInfo.path, destination); - - const normalizedTarget = toPosixPath(sanitizedTarget); - const publicPath = `${ASSETS_PUBLIC_PREFIX}/${normalizedTarget}`; - record = { destination, publicPath }; - this.cache.set(assetPathInfo.path, record); - } - - return record.publicPath; - } - - rewriteContent({ pageFilePath, pagePath, content }) { - if (!content) { - return content; - } - - let updatedContent = content; - updatedContent = this.#rewriteMarkdownImages( - pageFilePath, - pagePath, - updatedContent, - ); - updatedContent = this.#rewriteHtmlImages( - pageFilePath, - pagePath, - updatedContent, - ); - updatedContent = this.#rewriteHtmlSources( - pageFilePath, - pagePath, - updatedContent, - ); - return updatedContent; - } - - #rewriteMarkdownImages(pageFilePath, pagePath, content) { - return content.replace(MARKDOWN_IMAGE_REGEX, (match, assetRef) => { - const rewritten = this.#copyAsset(pageFilePath, pagePath, assetRef); - return rewritten ? match.replace(assetRef, rewritten) : match; - }); - } - - #rewriteHtmlImages(pageFilePath, pagePath, content) { - return content.replace(HTML_IMAGE_REGEX, (match, assetRef) => { - const rewritten = this.#copyAsset(pageFilePath, pagePath, assetRef); - return rewritten ? match.replace(assetRef, rewritten) : match; - }); - } - - #rewriteHtmlSources(pageFilePath, pagePath, content) { - return content.replace(HTML_SOURCE_REGEX, (match, assetRef) => { - const variants = assetRef - .split(",") - .map((variant) => variant.trim()) - .filter(Boolean); - if (variants.length === 0) { - return match; - } - - const rewrittenVariants = variants.map((variant) => { - const [pathPart, descriptor] = variant.split(/\s+/, 2); - const rewrittenPath = this.#copyAsset(pageFilePath, pagePath, pathPart); - return rewrittenPath - ? [rewrittenPath, descriptor].filter(Boolean).join(" ") - : variant; - }); - - const rewrittenSrcSet = rewrittenVariants.join(", "); - return rewrittenSrcSet === assetRef - ? match - : match.replace(assetRef, rewrittenSrcSet); - }); - } - - #copyAsset(pageFilePath, pagePath, assetReference) { - if (!this.#isLocalAsset(assetReference)) { - return null; - } - - const { path: assetPath, suffix } = - this.#splitAssetReference(assetReference); - const logicalAssetPath = resolve(dirname(pageFilePath), assetPath); - const assetPathInfo = this.#resolveAssetPathInfo(logicalAssetPath); - if (!assetPathInfo) { - return null; - } - - const stats = statSync(assetPathInfo.path); - if (!stats.isFile()) { - return null; - } - - let record = this.cache.get(assetPathInfo.path); - if (!record) { - const sanitizedTarget = this.#sanitizeAssetTarget( - assetPathInfo.relativePath, - assetPathInfo.path, - ); - const destination = join(this.sitePath, ASSETS_ROOT, sanitizedTarget); - mkdirSync(dirname(destination), { recursive: true }); - this.siteFileManager.copyFile(assetPathInfo.path, destination); - - const normalizedTarget = toPosixPath(sanitizedTarget); - const publicPath = `${ASSETS_PUBLIC_PREFIX}/${normalizedTarget}`; - record = { destination, publicPath }; - this.cache.set(assetPathInfo.path, record); - } - - return `${record.publicPath}${suffix}`; - } - - #resolveAssetPathInfo(assetAbsolutePath) { - try { - return this.workspacePathResolver.resolveExistingWithinWorkspace( - assetAbsolutePath, - ); - } catch { - return null; - } - } - - #sanitizeAssetTarget(relativeAssetPath, assetAbsolutePath) { - const cleanedSegments = relativeAssetPath - .split(/[\\/]+/) - .map((segment) => segment.trim()) - .filter(Boolean) - .map((segment) => - segment.replace(/^\.+/, "").replace(/[^a-zA-Z0-9._-]/g, "-"), - ) - .filter(Boolean); - - if (cleanedSegments.length === 0) { - const fallback = basename(assetAbsolutePath).replace( - /[^a-zA-Z0-9._-]/g, - "-", - ); - cleanedSegments.push(fallback || `asset-${randomUUID()}`); - } - - return cleanedSegments.join("/"); - } - - #splitAssetReference(assetReference) { - const trimmed = assetReference.trim(); - const match = trimmed.match(/^([^?#]+)(\?[^#]+)?(#.+)?$/); - if (!match) { - return { path: trimmed, suffix: "" }; - } - - return { - path: match[1], - suffix: `${match[2] || ""}${match[3] || ""}`, - }; - } - - #isLocalAsset(assetReference) { - if (!assetReference) { - return false; - } - - const trimmed = assetReference.trim(); - if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("data:")) { - return false; - } - - if (trimmed.startsWith("{%") || trimmed.startsWith("{{")) { - return false; - } - - if (trimmed.startsWith("/")) { - return false; - } - - if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)) { - return false; - } - - return true; - } + constructor({ workspacePath, sitePath }) { + this.workspacePath = workspacePath; + this.sitePath = sitePath; + this.cache = new Map(); + this.workspacePathResolver = new WorkspacePathResolver({ workspacePath }); + this.siteFileManager = new SiteFileManager(); + } + + copyAssetFromWorkspace(assetAbsolutePath) { + if (!assetAbsolutePath) { + return null; + } + + const assetPathInfo = this.#resolveAssetPathInfo(assetAbsolutePath); + if (!assetPathInfo) { + return null; + } + + const stats = statSync(assetPathInfo.path); + if (!stats.isFile()) { + return null; + } + + let record = this.cache.get(assetPathInfo.path); + if (!record) { + const sanitizedTarget = this.#sanitizeAssetTarget( + assetPathInfo.relativePath, + assetPathInfo.path, + ); + const destination = join(this.sitePath, ASSETS_ROOT, sanitizedTarget); + mkdirSync(dirname(destination), { recursive: true }); + this.siteFileManager.copyFile(assetPathInfo.path, destination); + + const normalizedTarget = toPosixPath(sanitizedTarget); + const publicPath = `${ASSETS_PUBLIC_PREFIX}/${normalizedTarget}`; + record = { destination, publicPath }; + this.cache.set(assetPathInfo.path, record); + } + + return record.publicPath; + } + + rewriteContent({ pageFilePath, pagePath, content }) { + if (!content) { + return content; + } + + let updatedContent = content; + updatedContent = this.#rewriteMarkdownImages( + pageFilePath, + pagePath, + updatedContent, + ); + updatedContent = this.#rewriteHtmlImages( + pageFilePath, + pagePath, + updatedContent, + ); + updatedContent = this.#rewriteHtmlSources( + pageFilePath, + pagePath, + updatedContent, + ); + return updatedContent; + } + + #rewriteMarkdownImages(pageFilePath, pagePath, content) { + return content.replace(MARKDOWN_IMAGE_REGEX, (match, assetRef) => { + const rewritten = this.#copyAsset(pageFilePath, pagePath, assetRef); + return rewritten ? match.replace(assetRef, rewritten) : match; + }); + } + + #rewriteHtmlImages(pageFilePath, pagePath, content) { + return content.replace(HTML_IMAGE_REGEX, (match, assetRef) => { + const rewritten = this.#copyAsset(pageFilePath, pagePath, assetRef); + return rewritten ? match.replace(assetRef, rewritten) : match; + }); + } + + #rewriteHtmlSources(pageFilePath, pagePath, content) { + return content.replace(HTML_SOURCE_REGEX, (match, assetRef) => { + const variants = assetRef + .split(",") + .map((variant) => variant.trim()) + .filter(Boolean); + if (variants.length === 0) { + return match; + } + + const rewrittenVariants = variants.map((variant) => { + const [pathPart, descriptor] = variant.split(/\s+/, 2); + const rewrittenPath = this.#copyAsset(pageFilePath, pagePath, pathPart); + return rewrittenPath + ? [rewrittenPath, descriptor].filter(Boolean).join(" ") + : variant; + }); + + const rewrittenSrcSet = rewrittenVariants.join(", "); + return rewrittenSrcSet === assetRef + ? match + : match.replace(assetRef, rewrittenSrcSet); + }); + } + + #copyAsset(pageFilePath, _pagePath, assetReference) { + if (!this.#isLocalAsset(assetReference)) { + return null; + } + + const { path: assetPath, suffix } = + this.#splitAssetReference(assetReference); + const logicalAssetPath = resolve(dirname(pageFilePath), assetPath); + const assetPathInfo = this.#resolveAssetPathInfo(logicalAssetPath); + if (!assetPathInfo) { + return null; + } + + const stats = statSync(assetPathInfo.path); + if (!stats.isFile()) { + return null; + } + + let record = this.cache.get(assetPathInfo.path); + if (!record) { + const sanitizedTarget = this.#sanitizeAssetTarget( + assetPathInfo.relativePath, + assetPathInfo.path, + ); + const destination = join(this.sitePath, ASSETS_ROOT, sanitizedTarget); + mkdirSync(dirname(destination), { recursive: true }); + this.siteFileManager.copyFile(assetPathInfo.path, destination); + + const normalizedTarget = toPosixPath(sanitizedTarget); + const publicPath = `${ASSETS_PUBLIC_PREFIX}/${normalizedTarget}`; + record = { destination, publicPath }; + this.cache.set(assetPathInfo.path, record); + } + + return `${record.publicPath}${suffix}`; + } + + #resolveAssetPathInfo(assetAbsolutePath) { + try { + return this.workspacePathResolver.resolveExistingWithinWorkspace( + assetAbsolutePath, + ); + } catch { + return null; + } + } + + #sanitizeAssetTarget(relativeAssetPath, assetAbsolutePath) { + const cleanedSegments = relativeAssetPath + .split(/[\\/]+/) + .map((segment) => segment.trim()) + .filter(Boolean) + .map((segment) => + segment.replace(/^\.+/, "").replace(/[^a-zA-Z0-9._-]/g, "-"), + ) + .filter(Boolean); + + if (cleanedSegments.length === 0) { + const fallback = basename(assetAbsolutePath).replace( + /[^a-zA-Z0-9._-]/g, + "-", + ); + cleanedSegments.push(fallback || `asset-${randomUUID()}`); + } + + return cleanedSegments.join("/"); + } + + #splitAssetReference(assetReference) { + const trimmed = assetReference.trim(); + const match = trimmed.match(/^([^?#]+)(\?[^#]+)?(#.+)?$/); + if (!match) { + return { path: trimmed, suffix: "" }; + } + + return { + path: match[1], + suffix: `${match[2] || ""}${match[3] || ""}`, + }; + } + + #isLocalAsset(assetReference) { + if (!assetReference) { + return false; + } + + const trimmed = assetReference.trim(); + if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("data:")) { + return false; + } + + if (trimmed.startsWith("{%") || trimmed.startsWith("{{")) { + return false; + } + + if (trimmed.startsWith("/")) { + return false; + } + + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)) { + return false; + } + + return true; + } } function toPosixPath(value) { - return value.split(sep).join("/"); + return value.split(sep).join("/"); } module.exports = { - AssetManager, - toPosixPath, + AssetManager, + toPosixPath, }; diff --git a/actions/deploy/jekyll/asset-manager.test.js b/actions/deploy/jekyll/asset-manager.test.js index b8cc2e7..c21cf18 100644 --- a/actions/deploy/jekyll/asset-manager.test.js +++ b/actions/deploy/jekyll/asset-manager.test.js @@ -1,11 +1,11 @@ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { - mkdtempSync, - mkdirSync, - writeFileSync, - existsSync, - rmSync, + mkdtempSync, + mkdirSync, + writeFileSync, + existsSync, + rmSync, } = require("node:fs"); const { join } = require("node:path"); const { tmpdir } = require("node:os"); @@ -13,92 +13,92 @@ const { tmpdir } = require("node:os"); const { AssetManager } = require("./asset-manager"); function withTempDir(run) { - const tempDir = mkdtempSync(join(tmpdir(), "jekyll-asset-manager-")); + const tempDir = mkdtempSync(join(tmpdir(), "jekyll-asset-manager-")); - return Promise.resolve() - .then(() => run(tempDir)) - .finally(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); + return Promise.resolve() + .then(() => run(tempDir)) + .finally(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); } describe("asset-manager.js", () => { - describe("AssetManager.copyAssetFromWorkspace", () => { - it("copies a local asset once and returns its public path", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const sitePath = join(workspacePath, "_site"); - const assetPath = join(workspacePath, "images", "logo.svg"); + describe("AssetManager.copyAssetFromWorkspace", () => { + it("copies a local asset once and returns its public path", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const sitePath = join(workspacePath, "_site"); + const assetPath = join(workspacePath, "images", "logo.svg"); - mkdirSync(join(workspacePath, "images"), { recursive: true }); - mkdirSync(sitePath, { recursive: true }); - writeFileSync(assetPath, ""); + mkdirSync(join(workspacePath, "images"), { recursive: true }); + mkdirSync(sitePath, { recursive: true }); + writeFileSync(assetPath, ""); - const assetManager = new AssetManager({ workspacePath, sitePath }); + const assetManager = new AssetManager({ workspacePath, sitePath }); - const firstPublicPath = assetManager.copyAssetFromWorkspace(assetPath); - const secondPublicPath = assetManager.copyAssetFromWorkspace(assetPath); + const firstPublicPath = assetManager.copyAssetFromWorkspace(assetPath); + const secondPublicPath = assetManager.copyAssetFromWorkspace(assetPath); - assert.equal( - firstPublicPath, - "{{site.baseurl}}/assets/images/logo.svg", - ); - assert.equal(secondPublicPath, firstPublicPath); - assert.equal( - existsSync(join(sitePath, "assets", "images", "logo.svg")), - true, - ); - }); - }); - }); + assert.equal( + firstPublicPath, + "{{site.baseurl}}/assets/images/logo.svg", + ); + assert.equal(secondPublicPath, firstPublicPath); + assert.equal( + existsSync(join(sitePath, "assets", "images", "logo.svg")), + true, + ); + }); + }); + }); - describe("AssetManager.rewriteContent", () => { - it("rewrites local markdown, html, and srcset asset references", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const sitePath = join(workspacePath, "_site"); - const docsPath = join(workspacePath, "docs"); - const pageFilePath = join(docsPath, "guide.md"); - const pagePath = join(sitePath, "docs", "guide", "index.md"); + describe("AssetManager.rewriteContent", () => { + it("rewrites local markdown, html, and srcset asset references", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const sitePath = join(workspacePath, "_site"); + const docsPath = join(workspacePath, "docs"); + const pageFilePath = join(docsPath, "guide.md"); + const pagePath = join(sitePath, "docs", "guide", "index.md"); - mkdirSync(join(docsPath, "images"), { recursive: true }); - mkdirSync(join(docsPath, "media"), { recursive: true }); - mkdirSync(join(docsPath, "video"), { recursive: true }); - mkdirSync(sitePath, { recursive: true }); - writeFileSync(pageFilePath, "# Guide\n"); - writeFileSync(join(docsPath, "images", "diagram.png"), "png"); - writeFileSync(join(docsPath, "media", "photo.jpg"), "jpg"); - writeFileSync(join(docsPath, "video", "sample.mp4"), "mp4"); + mkdirSync(join(docsPath, "images"), { recursive: true }); + mkdirSync(join(docsPath, "media"), { recursive: true }); + mkdirSync(join(docsPath, "video"), { recursive: true }); + mkdirSync(sitePath, { recursive: true }); + writeFileSync(pageFilePath, "# Guide\n"); + writeFileSync(join(docsPath, "images", "diagram.png"), "png"); + writeFileSync(join(docsPath, "media", "photo.jpg"), "jpg"); + writeFileSync(join(docsPath, "video", "sample.mp4"), "mp4"); - const assetManager = new AssetManager({ workspacePath, sitePath }); - const content = [ - "![Diagram](images/diagram.png)", - 'Photo', - '', - "![Remote](https://example.com/logo.png)", - ].join("\n"); + const assetManager = new AssetManager({ workspacePath, sitePath }); + const content = [ + "![Diagram](images/diagram.png)", + 'Photo', + '', + "![Remote](https://example.com/logo.png)", + ].join("\n"); - const rewritten = assetManager.rewriteContent({ - pageFilePath, - pagePath, - content, - }); + const rewritten = assetManager.rewriteContent({ + pageFilePath, + pagePath, + content, + }); - assert.match( - rewritten, - /\{\{site\.baseurl}}\/assets\/docs\/images\/diagram\.png/, - ); - assert.match( - rewritten, - /\{\{site\.baseurl}}\/assets\/docs\/media\/photo\.jpg/, - ); - assert.match( - rewritten, - /\{\{site\.baseurl}}\/assets\/docs\/video\/sample\.mp4 1x/, - ); - assert.match(rewritten, /https:\/\/example\.com\/remote\.mp4 2x/); - assert.match(rewritten, /https:\/\/example\.com\/logo\.png/); - }); - }); - }); + assert.match( + rewritten, + /\{\{site\.baseurl}}\/assets\/docs\/images\/diagram\.png/, + ); + assert.match( + rewritten, + /\{\{site\.baseurl}}\/assets\/docs\/media\/photo\.jpg/, + ); + assert.match( + rewritten, + /\{\{site\.baseurl}}\/assets\/docs\/video\/sample\.mp4 1x/, + ); + assert.match(rewritten, /https:\/\/example\.com\/remote\.mp4 2x/); + assert.match(rewritten, /https:\/\/example\.com\/logo\.png/); + }); + }); + }); }); diff --git a/actions/deploy/jekyll/package-lock.json b/actions/deploy/jekyll/package-lock.json index 13d1c26..51e3f51 100644 --- a/actions/deploy/jekyll/package-lock.json +++ b/actions/deploy/jekyll/package-lock.json @@ -1,16 +1,16 @@ { - "name": "deploy-jekyll", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "deploy-jekyll", - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=24.0.0" - } - } - } + "name": "deploy-jekyll", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deploy-jekyll", + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + } + } + } } diff --git a/actions/deploy/jekyll/package.json b/actions/deploy/jekyll/package.json index 372ec13..9adafa1 100644 --- a/actions/deploy/jekyll/package.json +++ b/actions/deploy/jekyll/package.json @@ -1,20 +1,20 @@ { - "name": "deploy-jekyll", - "version": "1.0.0", - "description": "Tests for the deploy/jekyll GitHub Action", - "scripts": { - "test": "node --test", - "test:watch": "node --test --watch", - "test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test" - }, - "keywords": [ - "github-action", - "jekyll", - "static-site" - ], - "author": "hoverkraft", - "license": "MIT", - "engines": { - "node": ">=24.0.0" - } + "name": "deploy-jekyll", + "version": "1.0.0", + "description": "Tests for the deploy/jekyll GitHub Action", + "scripts": { + "test": "node --test", + "test:watch": "node --test --watch", + "test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test" + }, + "keywords": [ + "github-action", + "jekyll", + "static-site" + ], + "author": "hoverkraft", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + } } diff --git a/actions/deploy/jekyll/page-files.js b/actions/deploy/jekyll/page-files.js index 607c258..8b2b206 100644 --- a/actions/deploy/jekyll/page-files.js +++ b/actions/deploy/jekyll/page-files.js @@ -1,5 +1,12 @@ -const { join, relative, basename, extname, dirname, resolve } = require("path"); -const { readFileSync } = require("fs"); +const { + join, + relative, + basename, + extname, + dirname, + resolve, +} = require("node:path"); +const { readFileSync } = require("node:fs"); const { toPosixPath } = require("./asset-manager"); const { SiteFileManager } = require("./site-file-manager"); @@ -9,142 +16,142 @@ const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".mdown", ".mkd"]); const siteFileManager = new SiteFileManager(); async function createSitePage({ - io, - assetManager, - pageFilePath, - pageTitle, - pagePath, - sitePath, - workspacePath, + io, + assetManager, + pageFilePath, + pageTitle, + pagePath, + sitePath, + workspacePath, }) { - const resolvedPageFilePath = resolve(pageFilePath); - const targetPagePath = - pagePath || - getPageSection({ - pageFilePath: resolvedPageFilePath, - sitePath, - workspacePath, - }); - const effectiveTitle = - pageTitle || - getPageTitle({ - pageFilePath: resolvedPageFilePath, - sitePath, - workspacePath, - }); - - await io.mkdirP(dirname(targetPagePath)); - - const rawContent = readFileSync(resolvedPageFilePath, "utf8"); - const safeContent = isMarkdownPage(resolvedPageFilePath) - ? escapeLiquidTags(rawContent) - : rawContent; - const contentWithFrontMatter = `---\nlayout: default\ntitle: ${effectiveTitle}\n---\n\n${safeContent}`; - - const processedContent = assetManager.rewriteContent({ - pageFilePath: resolvedPageFilePath, - pagePath: targetPagePath, - content: contentWithFrontMatter, - }); - - siteFileManager.writeFile(targetPagePath, processedContent); - return targetPagePath; + const resolvedPageFilePath = resolve(pageFilePath); + const targetPagePath = + pagePath || + getPageSection({ + pageFilePath: resolvedPageFilePath, + sitePath, + workspacePath, + }); + const effectiveTitle = + pageTitle || + getPageTitle({ + pageFilePath: resolvedPageFilePath, + sitePath, + workspacePath, + }); + + await io.mkdirP(dirname(targetPagePath)); + + const rawContent = readFileSync(resolvedPageFilePath, "utf8"); + const safeContent = isMarkdownPage(resolvedPageFilePath) + ? escapeLiquidTags(rawContent) + : rawContent; + const contentWithFrontMatter = `---\nlayout: default\ntitle: ${effectiveTitle}\n---\n\n${safeContent}`; + + const processedContent = assetManager.rewriteContent({ + pageFilePath: resolvedPageFilePath, + pagePath: targetPagePath, + content: contentWithFrontMatter, + }); + + siteFileManager.writeFile(targetPagePath, processedContent); + return targetPagePath; } function rewritePageLinks({ content, pagePath, pageMappings }) { - if (!content || pageMappings.size === 0) { - return content; - } + if (!content || pageMappings.size === 0) { + return content; + } - let updatedContent = content; - const currentDir = dirname(pagePath); + let updatedContent = content; + const currentDir = dirname(pagePath); - for (const [pageFileRelative, targetDir] of pageMappings.entries()) { - if (!pageFileRelative) { - continue; - } + for (const [pageFileRelative, targetDir] of pageMappings.entries()) { + if (!pageFileRelative) { + continue; + } - const relativeTarget = relative(currentDir, targetDir); - if (!relativeTarget) { - continue; - } + const relativeTarget = relative(currentDir, targetDir); + if (!relativeTarget) { + continue; + } - const replacement = formatLinkReplacement(relativeTarget); - updatedContent = updatedContent.split(pageFileRelative).join(replacement); - } + const replacement = formatLinkReplacement(relativeTarget); + updatedContent = updatedContent.split(pageFileRelative).join(replacement); + } - return updatedContent; + return updatedContent; } function getPageSection({ pageFilePath, sitePath, workspacePath }) { - const sectionParentDir = toSafeSegment( - relative(workspacePath, dirname(pageFilePath)), - ); - const sectionName = basename( - pageFilePath, - extname(pageFilePath), - ).toLowerCase(); - const sectionDir = sectionName !== "readme" ? toSafeSegment(sectionName) : ""; - const indexBasename = getIndexBasename(pageFilePath); - - const segments = [sectionParentDir, sectionDir, indexBasename].filter( - Boolean, - ); - return join(sitePath || join(workspacePath, "_site"), ...segments); + const sectionParentDir = toSafeSegment( + relative(workspacePath, dirname(pageFilePath)), + ); + const sectionName = basename( + pageFilePath, + extname(pageFilePath), + ).toLowerCase(); + const sectionDir = sectionName !== "readme" ? toSafeSegment(sectionName) : ""; + const indexBasename = getIndexBasename(pageFilePath); + + const segments = [sectionParentDir, sectionDir, indexBasename].filter( + Boolean, + ); + return join(sitePath || join(workspacePath, "_site"), ...segments); } function getIndexBasename(pageFilePath) { - const ext = extname(pageFilePath).toLowerCase(); - if (ext === ".html" || ext === ".htm") { - return "index.html"; - } + const ext = extname(pageFilePath).toLowerCase(); + if (ext === ".html" || ext === ".htm") { + return "index.html"; + } - return INDEX_BASENAME; + return INDEX_BASENAME; } function getPageTitle({ pageFilePath, sitePath, workspacePath }) { - const sectionPath = getPageSection({ pageFilePath, sitePath, workspacePath }); - const sectionDir = dirname(sectionPath); - const sectionName = basename(sectionDir); - - return sectionName - .split("-") - .filter(Boolean) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); + const sectionPath = getPageSection({ pageFilePath, sitePath, workspacePath }); + const sectionDir = dirname(sectionPath); + const sectionName = basename(sectionDir); + + return sectionName + .split("-") + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); } function toSafeSegment(value) { - return value - .toLowerCase() - .split(/[\\/]+/) - .map((segment) => segment.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")) - .filter(Boolean) - .join("/"); + return value + .toLowerCase() + .split(/[\\/]+/) + .map((segment) => segment.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")) + .filter(Boolean) + .join("/"); } function formatLinkReplacement(relativePath) { - const normalized = toPosixPath(relativePath); - return normalized || "."; + const normalized = toPosixPath(relativePath); + return normalized || "."; } function isMarkdownPage(pageFilePath) { - return MARKDOWN_EXTENSIONS.has(extname(pageFilePath).toLowerCase()); + return MARKDOWN_EXTENSIONS.has(extname(pageFilePath).toLowerCase()); } function escapeLiquidTags(content) { - if (!content) { - return content; - } + if (!content) { + return content; + } - return content.replace(LIQUID_TAG_PATTERN, (_match, tag) => { - return `{% raw %}${tag}{% endraw %}`; - }); + return content.replace(LIQUID_TAG_PATTERN, (_match, tag) => { + return `{% raw %}${tag}{% endraw %}`; + }); } module.exports = { - INDEX_BASENAME, - createSitePage, - rewritePageLinks, - toPosixPath, + INDEX_BASENAME, + createSitePage, + rewritePageLinks, + toPosixPath, }; diff --git a/actions/deploy/jekyll/page-files.test.js b/actions/deploy/jekyll/page-files.test.js index d0749d1..83ff398 100644 --- a/actions/deploy/jekyll/page-files.test.js +++ b/actions/deploy/jekyll/page-files.test.js @@ -1,12 +1,12 @@ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { - mkdtempSync, - mkdirSync, - writeFileSync, - readFileSync, - existsSync, - rmSync, + mkdtempSync, + mkdirSync, + writeFileSync, + readFileSync, + existsSync, + rmSync, } = require("node:fs"); const { join } = require("node:path"); const { tmpdir } = require("node:os"); @@ -15,92 +15,92 @@ const { AssetManager } = require("./asset-manager"); const { createSitePage, rewritePageLinks } = require("./page-files"); function withTempDir(run) { - const tempDir = mkdtempSync(join(tmpdir(), "jekyll-page-files-")); + const tempDir = mkdtempSync(join(tmpdir(), "jekyll-page-files-")); - return Promise.resolve() - .then(() => run(tempDir)) - .finally(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); + return Promise.resolve() + .then(() => run(tempDir)) + .finally(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); } function createIoStub() { - return { - async mkdirP(targetPath) { - mkdirSync(targetPath, { recursive: true }); - }, - }; + return { + async mkdirP(targetPath) { + mkdirSync(targetPath, { recursive: true }); + }, + }; } describe("page-files.js", () => { - describe("createSitePage", () => { - it("writes front matter, escapes Liquid tags, and copies local assets", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const sitePath = join(workspacePath, "_site"); - const docsPath = join(workspacePath, "docs"); - const pageFilePath = join(docsPath, "guide.md"); + describe("createSitePage", () => { + it("writes front matter, escapes Liquid tags, and copies local assets", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const sitePath = join(workspacePath, "_site"); + const docsPath = join(workspacePath, "docs"); + const pageFilePath = join(docsPath, "guide.md"); - mkdirSync(join(docsPath, "images"), { recursive: true }); - mkdirSync(sitePath, { recursive: true }); - writeFileSync( - pageFilePath, - [ - "# Guide", - "", - "{% include note.html %}", - "", - "![Diagram](images/diagram.png)", - ].join("\n"), - ); - writeFileSync(join(docsPath, "images", "diagram.png"), "png"); + mkdirSync(join(docsPath, "images"), { recursive: true }); + mkdirSync(sitePath, { recursive: true }); + writeFileSync( + pageFilePath, + [ + "# Guide", + "", + "{% include note.html %}", + "", + "![Diagram](images/diagram.png)", + ].join("\n"), + ); + writeFileSync(join(docsPath, "images", "diagram.png"), "png"); - const assetManager = new AssetManager({ workspacePath, sitePath }); + const assetManager = new AssetManager({ workspacePath, sitePath }); - const createdPagePath = await createSitePage({ - io: createIoStub(), - assetManager, - pageFilePath, - workspacePath, - }); + const createdPagePath = await createSitePage({ + io: createIoStub(), + assetManager, + pageFilePath, + workspacePath, + }); - const createdContent = readFileSync(createdPagePath, "utf8"); + const createdContent = readFileSync(createdPagePath, "utf8"); - assert.equal( - createdPagePath, - join(workspacePath, "_site", "docs", "guide", "index.md"), - ); - assert.match( - createdContent, - /^---\nlayout: default\ntitle: Guide\n---/, - ); - assert.match( - createdContent, - /\{% raw %}\{% include note\.html %}\{% endraw %}/, - ); - assert.match( - createdContent, - /\{\{site\.baseurl}}\/assets\/docs\/images\/diagram\.png/, - ); - assert.equal( - existsSync(join(sitePath, "assets", "docs", "images", "diagram.png")), - true, - ); - }); - }); - }); + assert.equal( + createdPagePath, + join(workspacePath, "_site", "docs", "guide", "index.md"), + ); + assert.match( + createdContent, + /^---\nlayout: default\ntitle: Guide\n---/, + ); + assert.match( + createdContent, + /\{% raw %}\{% include note\.html %}\{% endraw %}/, + ); + assert.match( + createdContent, + /\{\{site\.baseurl}}\/assets\/docs\/images\/diagram\.png/, + ); + assert.equal( + existsSync(join(sitePath, "assets", "docs", "images", "diagram.png")), + true, + ); + }); + }); + }); - describe("rewritePageLinks", () => { - it("rewrites generated page links relative to the current page", () => { - const rewritten = rewritePageLinks({ - content: "See [Target](docs/tests/target.md) for details.", - pagePath: "/workspace/_site/docs/tests/source/index.md", - pageMappings: new Map([ - ["docs/tests/target.md", "/workspace/_site/docs/tests/target"], - ]), - }); + describe("rewritePageLinks", () => { + it("rewrites generated page links relative to the current page", () => { + const rewritten = rewritePageLinks({ + content: "See [Target](docs/tests/target.md) for details.", + pagePath: "/workspace/_site/docs/tests/source/index.md", + pageMappings: new Map([ + ["docs/tests/target.md", "/workspace/_site/docs/tests/target"], + ]), + }); - assert.equal(rewritten, "See [Target](../target) for details."); - }); - }); + assert.equal(rewritten, "See [Target](../target) for details."); + }); + }); }); diff --git a/actions/deploy/jekyll/prepare-site.js b/actions/deploy/jekyll/prepare-site.js index e0b67e2..c991e21 100644 --- a/actions/deploy/jekyll/prepare-site.js +++ b/actions/deploy/jekyll/prepare-site.js @@ -1,247 +1,247 @@ -const { join, relative, dirname, resolve } = require("path"); -const { existsSync, readFileSync } = require("fs"); +const { join, relative, dirname, resolve } = require("node:path"); +const { existsSync, readFileSync } = require("node:fs"); const { - INDEX_BASENAME, - createSitePage, - rewritePageLinks, + INDEX_BASENAME, + createSitePage, + rewritePageLinks, } = require("./page-files"); const { AssetManager, toPosixPath } = require("./asset-manager"); const { SiteFileManager } = require("./site-file-manager"); const { WorkspacePathResolver } = require("./workspace-path-resolver"); const DEFAULT_THEME = "jekyll-theme-cayman"; module.exports = async function prepareSite({ core, inputs, io, glob }) { - const workspacePath = resolveWorkspace(); - const workspacePathResolver = new WorkspacePathResolver({ workspacePath }); - const { theme, pagePatterns, assetPatterns, sitePathInput, buildPathInput } = - normalizeInputs(inputs); - const { sitePath, buildPath } = resolveSitePaths(workspacePathResolver, { - sitePathInput, - buildPathInput, - }); - const assetManager = new AssetManager({ workspacePath, sitePath }); - const siteFileManager = new SiteFileManager(); - - core.debug( - `Configuration: ${JSON.stringify({ - theme, - pagePatterns, - assetPatterns, - sitePath: toPosixPath(relative(workspacePath, sitePath)), - buildPath: toPosixPath(relative(workspacePath, buildPath)), - })}`, - ); - - core.setOutput("jekyll-source", relative(workspacePath, sitePath)); - core.setOutput("jekyll-destination", relative(workspacePath, buildPath)); - - await io.mkdirP(sitePath); - ensureConfigFile({ core, sitePath, theme }); - - if (assetPatterns.length > 0) { - const assetsGlobber = await glob.create(assetPatterns.join("\n")); - const copiedAssets = []; - for await (const assetFile of assetsGlobber.globGenerator()) { - const assetPathInfo = - workspacePathResolver.resolveExistingWithinWorkspace(assetFile); - const publicPath = await assetManager.copyAssetFromWorkspace( - assetPathInfo.path, - ); - if (publicPath) { - copiedAssets.push({ - source: toPosixPath(assetPathInfo.relativePath), - target: publicPath, - }); - } - } - - core.debug( - `Copied ${copiedAssets.length} additional assets: ${JSON.stringify(copiedAssets)}`, - ); - } - - const indexPath = join(sitePath, INDEX_BASENAME); - if (!existsSync(indexPath)) { - await createSitePage({ - assetManager, - io, - pageFilePath: - workspacePathResolver.resolveExistingWithinWorkspace("README.md").path, - pageTitle: "Home", - pagePath: indexPath, - sitePath, - workspacePath, - }); - - core.debug( - `Created site index from README.md at ${toPosixPath(relative(workspacePath, indexPath))}`, - ); - } else { - core.debug( - `Using existing site index at ${toPosixPath(relative(workspacePath, indexPath))}`, - ); - } - - if (pagePatterns.length === 0) { - core.debug("No additional page patterns configured."); - return; - } - - const globber = await glob.create(pagePatterns.join("\n")); - const indexContent = readFileSync(indexPath, "utf8"); - const pageMappings = new Map(); - const createdPagePaths = []; - - for await (const pageFile of globber.globGenerator()) { - const pagePathInfo = - workspacePathResolver.resolveExistingWithinWorkspace(pageFile); - const createdPagePath = await createSitePage({ - assetManager, - io, - pageFilePath: pagePathInfo.path, - sitePath, - workspacePath, - }); - - addPageMapping( - pageMappings, - toPosixPath(pagePathInfo.relativePath), - dirname(createdPagePath), - ); - - createdPagePaths.push(createdPagePath); - } - - core.debug( - `Prepared ${createdPagePaths.length} additional pages from patterns ${JSON.stringify(pagePatterns)}: ${JSON.stringify( - createdPagePaths.map((pagePath) => - toPosixPath(relative(workspacePath, pagePath)), - ), - )}`, - ); - - if (pageMappings.size === 0) { - core.debug("No additional pages matched the configured patterns."); - return; - } - - const rewrittenIndexContent = rewritePageLinks({ - content: indexContent, - pagePath: indexPath, - pageMappings, - }); - - if (rewrittenIndexContent !== indexContent) { - siteFileManager.writeFile(indexPath, rewrittenIndexContent); - } - - for (const pagePath of createdPagePaths) { - const pageContent = readFileSync(pagePath, "utf8"); - const rewrittenContent = rewritePageLinks({ - content: pageContent, - pagePath, - pageMappings, - }); - - if (rewrittenContent !== pageContent) { - siteFileManager.writeFile(pagePath, rewrittenContent); - } - } + const workspacePath = resolveWorkspace(); + const workspacePathResolver = new WorkspacePathResolver({ workspacePath }); + const { theme, pagePatterns, assetPatterns, sitePathInput, buildPathInput } = + normalizeInputs(inputs); + const { sitePath, buildPath } = resolveSitePaths(workspacePathResolver, { + sitePathInput, + buildPathInput, + }); + const assetManager = new AssetManager({ workspacePath, sitePath }); + const siteFileManager = new SiteFileManager(); + + core.debug( + `Configuration: ${JSON.stringify({ + theme, + pagePatterns, + assetPatterns, + sitePath: toPosixPath(relative(workspacePath, sitePath)), + buildPath: toPosixPath(relative(workspacePath, buildPath)), + })}`, + ); + + core.setOutput("jekyll-source", relative(workspacePath, sitePath)); + core.setOutput("jekyll-destination", relative(workspacePath, buildPath)); + + await io.mkdirP(sitePath); + ensureConfigFile({ core, sitePath, theme }); + + if (assetPatterns.length > 0) { + const assetsGlobber = await glob.create(assetPatterns.join("\n")); + const copiedAssets = []; + for await (const assetFile of assetsGlobber.globGenerator()) { + const assetPathInfo = + workspacePathResolver.resolveExistingWithinWorkspace(assetFile); + const publicPath = await assetManager.copyAssetFromWorkspace( + assetPathInfo.path, + ); + if (publicPath) { + copiedAssets.push({ + source: toPosixPath(assetPathInfo.relativePath), + target: publicPath, + }); + } + } + + core.debug( + `Copied ${copiedAssets.length} additional assets: ${JSON.stringify(copiedAssets)}`, + ); + } + + const indexPath = join(sitePath, INDEX_BASENAME); + if (!existsSync(indexPath)) { + await createSitePage({ + assetManager, + io, + pageFilePath: + workspacePathResolver.resolveExistingWithinWorkspace("README.md").path, + pageTitle: "Home", + pagePath: indexPath, + sitePath, + workspacePath, + }); + + core.debug( + `Created site index from README.md at ${toPosixPath(relative(workspacePath, indexPath))}`, + ); + } else { + core.debug( + `Using existing site index at ${toPosixPath(relative(workspacePath, indexPath))}`, + ); + } + + if (pagePatterns.length === 0) { + core.debug("No additional page patterns configured."); + return; + } + + const globber = await glob.create(pagePatterns.join("\n")); + const indexContent = readFileSync(indexPath, "utf8"); + const pageMappings = new Map(); + const createdPagePaths = []; + + for await (const pageFile of globber.globGenerator()) { + const pagePathInfo = + workspacePathResolver.resolveExistingWithinWorkspace(pageFile); + const createdPagePath = await createSitePage({ + assetManager, + io, + pageFilePath: pagePathInfo.path, + sitePath, + workspacePath, + }); + + addPageMapping( + pageMappings, + toPosixPath(pagePathInfo.relativePath), + dirname(createdPagePath), + ); + + createdPagePaths.push(createdPagePath); + } + + core.debug( + `Prepared ${createdPagePaths.length} additional pages from patterns ${JSON.stringify(pagePatterns)}: ${JSON.stringify( + createdPagePaths.map((pagePath) => + toPosixPath(relative(workspacePath, pagePath)), + ), + )}`, + ); + + if (pageMappings.size === 0) { + core.debug("No additional pages matched the configured patterns."); + return; + } + + const rewrittenIndexContent = rewritePageLinks({ + content: indexContent, + pagePath: indexPath, + pageMappings, + }); + + if (rewrittenIndexContent !== indexContent) { + siteFileManager.writeFile(indexPath, rewrittenIndexContent); + } + + for (const pagePath of createdPagePaths) { + const pageContent = readFileSync(pagePath, "utf8"); + const rewrittenContent = rewritePageLinks({ + content: pageContent, + pagePath, + pageMappings, + }); + + if (rewrittenContent !== pageContent) { + siteFileManager.writeFile(pagePath, rewrittenContent); + } + } }; function resolveWorkspace() { - const workspacePath = process.env.GITHUB_WORKSPACE; - if (!workspacePath) { - throw new Error("GITHUB_WORKSPACE environment variable is not defined."); - } + const workspacePath = process.env.GITHUB_WORKSPACE; + if (!workspacePath) { + throw new Error("GITHUB_WORKSPACE environment variable is not defined."); + } - return resolve(workspacePath); + return resolve(workspacePath); } function normalizeInputs(rawInputs = {}) { - const theme = (rawInputs.theme || DEFAULT_THEME).trim(); - const pagesInput = (rawInputs.pages || "").trim(); - const pagePatterns = pagesInput - ? pagesInput.split(/\s+/).filter(Boolean) - : []; - - const assetsInput = (rawInputs.assets || "").trim(); - const assetPatterns = assetsInput - ? assetsInput.split(/\s+/).filter(Boolean) - : []; - - const sitePathInput = requireNonEmptyInput( - rawInputs["site-path"], - "site-path", - ); - const buildPathInput = requireNonEmptyInput( - rawInputs["build-path"], - "build-path", - ); - - return { - theme, - pagePatterns, - assetPatterns, - sitePathInput, - buildPathInput, - }; + const theme = (rawInputs.theme || DEFAULT_THEME).trim(); + const pagesInput = (rawInputs.pages || "").trim(); + const pagePatterns = pagesInput + ? pagesInput.split(/\s+/).filter(Boolean) + : []; + + const assetsInput = (rawInputs.assets || "").trim(); + const assetPatterns = assetsInput + ? assetsInput.split(/\s+/).filter(Boolean) + : []; + + const sitePathInput = requireNonEmptyInput( + rawInputs["site-path"], + "site-path", + ); + const buildPathInput = requireNonEmptyInput( + rawInputs["build-path"], + "build-path", + ); + + return { + theme, + pagePatterns, + assetPatterns, + sitePathInput, + buildPathInput, + }; } function resolveSitePaths( - workspacePathResolver, - { sitePathInput, buildPathInput }, + workspacePathResolver, + { sitePathInput, buildPathInput }, ) { - const sitePath = workspacePathResolver.resolveWithinWorkspace(sitePathInput); - const buildPath = - workspacePathResolver.resolveWithinWorkspace(buildPathInput); + const sitePath = workspacePathResolver.resolveWithinWorkspace(sitePathInput); + const buildPath = + workspacePathResolver.resolveWithinWorkspace(buildPathInput); - return { sitePath, buildPath }; + return { sitePath, buildPath }; } function requireNonEmptyInput(value, inputName) { - if (typeof value !== "string") { - throw new Error(`${inputName} input is required.`); - } + if (typeof value !== "string") { + throw new Error(`${inputName} input is required.`); + } - const trimmed = value.trim(); - if (!trimmed) { - throw new Error(`${inputName} input cannot be empty.`); - } + const trimmed = value.trim(); + if (!trimmed) { + throw new Error(`${inputName} input cannot be empty.`); + } - return trimmed; + return trimmed; } function addPageMapping(pageMappings, pageFileRelative, targetDir) { - if (!pageFileRelative || pageMappings.has(pageFileRelative)) { - return; - } + if (!pageFileRelative || pageMappings.has(pageFileRelative)) { + return; + } - pageMappings.set(pageFileRelative, targetDir); + pageMappings.set(pageFileRelative, targetDir); } function ensureConfigFile({ core, sitePath, theme }) { - const siteFileManager = new SiteFileManager(); - const configPath = join(sitePath, "_config.yml"); - if (existsSync(configPath)) { - core.debug( - `Using existing Jekyll config at ${toPosixPath(relative(resolveWorkspace(), configPath))}`, - ); - return; - } - - if (!theme) { - throw new Error("Theme input is required."); - } - - const isGitHubSupportedTheme = theme.startsWith("jekyll-theme-"); - const configContent = isGitHubSupportedTheme - ? `theme: ${theme}` - : `remote_theme: ${theme}\nplugins:\n - jekyll-remote-theme`; - - siteFileManager.writeFile(configPath, configContent); - - core.debug( - `Created Jekyll config at ${toPosixPath(relative(resolveWorkspace(), configPath))} with theme ${theme}`, - ); + const siteFileManager = new SiteFileManager(); + const configPath = join(sitePath, "_config.yml"); + if (existsSync(configPath)) { + core.debug( + `Using existing Jekyll config at ${toPosixPath(relative(resolveWorkspace(), configPath))}`, + ); + return; + } + + if (!theme) { + throw new Error("Theme input is required."); + } + + const isGitHubSupportedTheme = theme.startsWith("jekyll-theme-"); + const configContent = isGitHubSupportedTheme + ? `theme: ${theme}` + : `remote_theme: ${theme}\nplugins:\n - jekyll-remote-theme`; + + siteFileManager.writeFile(configPath, configContent); + + core.debug( + `Created Jekyll config at ${toPosixPath(relative(resolveWorkspace(), configPath))} with theme ${theme}`, + ); } diff --git a/actions/deploy/jekyll/prepare-site.test.js b/actions/deploy/jekyll/prepare-site.test.js index 9c56fd0..868de6b 100644 --- a/actions/deploy/jekyll/prepare-site.test.js +++ b/actions/deploy/jekyll/prepare-site.test.js @@ -1,12 +1,12 @@ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { - mkdtempSync, - mkdirSync, - writeFileSync, - readFileSync, - existsSync, - rmSync, + mkdtempSync, + mkdirSync, + writeFileSync, + readFileSync, + existsSync, + rmSync, } = require("node:fs"); const { join } = require("node:path"); const { tmpdir } = require("node:os"); @@ -14,152 +14,152 @@ const { tmpdir } = require("node:os"); const prepareSite = require("./prepare-site"); function withTempDir(run) { - const tempDir = mkdtempSync(join(tmpdir(), "jekyll-prepare-site-")); + const tempDir = mkdtempSync(join(tmpdir(), "jekyll-prepare-site-")); - return Promise.resolve() - .then(() => run(tempDir)) - .finally(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); + return Promise.resolve() + .then(() => run(tempDir)) + .finally(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); } function createCoreStub() { - const calls = { - debug: [], - outputs: {}, - }; + const calls = { + debug: [], + outputs: {}, + }; - return { - calls, - debug(message) { - calls.debug.push(message); - }, - setOutput(name, value) { - calls.outputs[name] = value; - }, - }; + return { + calls, + debug(message) { + calls.debug.push(message); + }, + setOutput(name, value) { + calls.outputs[name] = value; + }, + }; } function createIoStub() { - return { - async mkdirP(targetPath) { - mkdirSync(targetPath, { recursive: true }); - }, - }; + return { + async mkdirP(targetPath) { + mkdirSync(targetPath, { recursive: true }); + }, + }; } function createGlobStub() { - return { - async create(patterns) { - const entries = patterns - .split("\n") - .map((entry) => entry.trim()) - .filter(Boolean); + return { + async create(patterns) { + const entries = patterns + .split("\n") + .map((entry) => entry.trim()) + .filter(Boolean); - return { - async *globGenerator() { - for (const entry of entries) { - yield entry; - } - }, - }; - }, - }; + return { + async *globGenerator() { + for (const entry of entries) { + yield entry; + } + }, + }; + }, + }; } describe("prepare-site.js", () => { - describe("prepareSite", () => { - it("creates the site, rewrites links, copies assets, and sets outputs", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const docsPath = join(workspacePath, "docs", "tests"); - const imagesPath = join(workspacePath, "images"); - const previousWorkspace = process.env.GITHUB_WORKSPACE; + describe("prepareSite", () => { + it("creates the site, rewrites links, copies assets, and sets outputs", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const docsPath = join(workspacePath, "docs", "tests"); + const imagesPath = join(workspacePath, "images"); + const previousWorkspace = process.env.GITHUB_WORKSPACE; - mkdirSync(docsPath, { recursive: true }); - mkdirSync(imagesPath, { recursive: true }); - writeFileSync( - join(workspacePath, "README.md"), - ["# Home", "", "- [Source](docs/tests/source.md)"].join("\n"), - ); - writeFileSync( - join(docsPath, "source.md"), - [ - "# Source", - "", - "Link to [Target](docs/tests/target.md).", - "", - "![Logo](../../images/logo.svg)", - ].join("\n"), - ); - writeFileSync(join(docsPath, "target.md"), "# Target\n"); - writeFileSync(join(imagesPath, "logo.svg"), ""); + mkdirSync(docsPath, { recursive: true }); + mkdirSync(imagesPath, { recursive: true }); + writeFileSync( + join(workspacePath, "README.md"), + ["# Home", "", "- [Source](docs/tests/source.md)"].join("\n"), + ); + writeFileSync( + join(docsPath, "source.md"), + [ + "# Source", + "", + "Link to [Target](docs/tests/target.md).", + "", + "![Logo](../../images/logo.svg)", + ].join("\n"), + ); + writeFileSync(join(docsPath, "target.md"), "# Target\n"); + writeFileSync(join(imagesPath, "logo.svg"), ""); - process.env.GITHUB_WORKSPACE = workspacePath; + process.env.GITHUB_WORKSPACE = workspacePath; - const core = createCoreStub(); + const core = createCoreStub(); - try { - await prepareSite({ - core, - io: createIoStub(), - glob: createGlobStub(), - inputs: { - theme: "acme/theme", - pages: "docs/tests/source.md\ndocs/tests/target.md", - assets: "images/logo.svg", - "site-path": "custom-site", - "build-path": "public-build", - }, - }); - } finally { - if (previousWorkspace === undefined) { - delete process.env.GITHUB_WORKSPACE; - } else { - process.env.GITHUB_WORKSPACE = previousWorkspace; - } - } + try { + await prepareSite({ + core, + io: createIoStub(), + glob: createGlobStub(), + inputs: { + theme: "acme/theme", + pages: "docs/tests/source.md\ndocs/tests/target.md", + assets: "images/logo.svg", + "site-path": "custom-site", + "build-path": "public-build", + }, + }); + } finally { + if (previousWorkspace === undefined) { + delete process.env.GITHUB_WORKSPACE; + } else { + process.env.GITHUB_WORKSPACE = previousWorkspace; + } + } - const sitePath = join(workspacePath, "custom-site"); - const configContent = readFileSync( - join(sitePath, "_config.yml"), - "utf8", - ); - const indexContent = readFileSync(join(sitePath, "index.md"), "utf8"); - const sourceContent = readFileSync( - join(sitePath, "docs", "tests", "source", "index.md"), - "utf8", - ); + const sitePath = join(workspacePath, "custom-site"); + const configContent = readFileSync( + join(sitePath, "_config.yml"), + "utf8", + ); + const indexContent = readFileSync(join(sitePath, "index.md"), "utf8"); + const sourceContent = readFileSync( + join(sitePath, "docs", "tests", "source", "index.md"), + "utf8", + ); - assert.equal(core.calls.outputs["jekyll-source"], "custom-site"); - assert.equal(core.calls.outputs["jekyll-destination"], "public-build"); - assert.match(configContent, /remote_theme: acme\/theme/); - assert.match(configContent, /jekyll-remote-theme/); - assert.match(indexContent, /\]\(docs\/tests\/source\)/); - assert.match(sourceContent, /\]\(\.\.\/target\)/); - assert.equal( - existsSync(join(sitePath, "assets", "images", "logo.svg")), - true, - ); - assert.equal( - core.calls.debug.some((message) => - message.includes("Configuration:"), - ), - true, - ); - assert.equal( - core.calls.debug.some((message) => - message.includes("Copied 1 additional assets"), - ), - true, - ); - assert.equal( - core.calls.debug.some((message) => - message.includes("Prepared 2 additional pages"), - ), - true, - ); - }); - }); - }); + assert.equal(core.calls.outputs["jekyll-source"], "custom-site"); + assert.equal(core.calls.outputs["jekyll-destination"], "public-build"); + assert.match(configContent, /remote_theme: acme\/theme/); + assert.match(configContent, /jekyll-remote-theme/); + assert.match(indexContent, /\]\(docs\/tests\/source\)/); + assert.match(sourceContent, /\]\(\.\.\/target\)/); + assert.equal( + existsSync(join(sitePath, "assets", "images", "logo.svg")), + true, + ); + assert.equal( + core.calls.debug.some((message) => + message.includes("Configuration:"), + ), + true, + ); + assert.equal( + core.calls.debug.some((message) => + message.includes("Copied 1 additional assets"), + ), + true, + ); + assert.equal( + core.calls.debug.some((message) => + message.includes("Prepared 2 additional pages"), + ), + true, + ); + }); + }); + }); }); diff --git a/actions/deploy/jekyll/site-file-manager.js b/actions/deploy/jekyll/site-file-manager.js index 509c46e..e042871 100644 --- a/actions/deploy/jekyll/site-file-manager.js +++ b/actions/deploy/jekyll/site-file-manager.js @@ -1,18 +1,18 @@ -const { copyFileSync, mkdirSync, writeFileSync } = require("fs"); -const { dirname } = require("path"); +const { copyFileSync, mkdirSync, writeFileSync } = require("node:fs"); +const { dirname } = require("node:path"); class SiteFileManager { - writeFile(targetPath, content) { - mkdirSync(dirname(targetPath), { recursive: true }); - writeFileSync(targetPath, content); - } + writeFile(targetPath, content) { + mkdirSync(dirname(targetPath), { recursive: true }); + writeFileSync(targetPath, content); + } - copyFile(sourcePath, targetPath) { - mkdirSync(dirname(targetPath), { recursive: true }); - copyFileSync(sourcePath, targetPath); - } + copyFile(sourcePath, targetPath) { + mkdirSync(dirname(targetPath), { recursive: true }); + copyFileSync(sourcePath, targetPath); + } } module.exports = { - SiteFileManager, + SiteFileManager, }; diff --git a/actions/deploy/jekyll/workspace-path-resolver.js b/actions/deploy/jekyll/workspace-path-resolver.js index 4b49295..0cfab3e 100644 --- a/actions/deploy/jekyll/workspace-path-resolver.js +++ b/actions/deploy/jekyll/workspace-path-resolver.js @@ -1,52 +1,52 @@ -const { existsSync } = require("fs"); -const { isAbsolute, relative, resolve, sep } = require("path"); +const { existsSync } = require("node:fs"); +const { isAbsolute, relative, resolve, sep } = require("node:path"); class WorkspacePathResolver { - constructor({ workspacePath }) { - if (!workspacePath) { - throw new Error("workspacePath is required."); - } - - this.workspacePath = resolve(workspacePath); - } - - resolveWithinWorkspace(targetPath) { - const resolvedPath = isAbsolute(targetPath) - ? resolve(targetPath) - : resolve(this.workspacePath, targetPath); - - this.#assertWithinWorkspace(resolvedPath); - return resolvedPath; - } - - resolveExistingWithinWorkspace(targetPath) { - const resolvedPath = this.resolveWithinWorkspace(targetPath); - - if (!existsSync(resolvedPath)) { - throw new Error(`Path does not exist: ${resolvedPath}`); - } - - return { - path: resolvedPath, - relativePath: relative(this.workspacePath, resolvedPath), - }; - } - - #assertWithinWorkspace(targetPath) { - const pathFromWorkspace = relative(this.workspacePath, targetPath); - - if ( - pathFromWorkspace === ".." || - pathFromWorkspace.startsWith(`..${sep}`) || - isAbsolute(pathFromWorkspace) - ) { - throw new Error( - `Path must stay within workspace. Provided value resolves to: ${targetPath}`, - ); - } - } + constructor({ workspacePath }) { + if (!workspacePath) { + throw new Error("workspacePath is required."); + } + + this.workspacePath = resolve(workspacePath); + } + + resolveWithinWorkspace(targetPath) { + const resolvedPath = isAbsolute(targetPath) + ? resolve(targetPath) + : resolve(this.workspacePath, targetPath); + + this.#assertWithinWorkspace(resolvedPath); + return resolvedPath; + } + + resolveExistingWithinWorkspace(targetPath) { + const resolvedPath = this.resolveWithinWorkspace(targetPath); + + if (!existsSync(resolvedPath)) { + throw new Error(`Path does not exist: ${resolvedPath}`); + } + + return { + path: resolvedPath, + relativePath: relative(this.workspacePath, resolvedPath), + }; + } + + #assertWithinWorkspace(targetPath) { + const pathFromWorkspace = relative(this.workspacePath, targetPath); + + if ( + pathFromWorkspace === ".." || + pathFromWorkspace.startsWith(`..${sep}`) || + isAbsolute(pathFromWorkspace) + ) { + throw new Error( + `Path must stay within workspace. Provided value resolves to: ${targetPath}`, + ); + } + } } module.exports = { - WorkspacePathResolver, + WorkspacePathResolver, }; diff --git a/actions/deploy/jekyll/workspace-path-resolver.test.js b/actions/deploy/jekyll/workspace-path-resolver.test.js index 6f6022a..c00dcb4 100644 --- a/actions/deploy/jekyll/workspace-path-resolver.test.js +++ b/actions/deploy/jekyll/workspace-path-resolver.test.js @@ -7,50 +7,50 @@ const { tmpdir } = require("node:os"); const { WorkspacePathResolver } = require("./workspace-path-resolver"); function withTempDir(run) { - const tempDir = mkdtempSync(join(tmpdir(), "jekyll-workspace-path-")); + const tempDir = mkdtempSync(join(tmpdir(), "jekyll-workspace-path-")); - return Promise.resolve() - .then(() => run(tempDir)) - .finally(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); + return Promise.resolve() + .then(() => run(tempDir)) + .finally(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); } describe("workspace-path-resolver.js", () => { - describe("WorkspacePathResolver.resolveExistingWithinWorkspace", () => { - it("resolves an existing path inside the workspace", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const filePath = join(workspacePath, "content", "guide.md"); - - mkdirSync(join(workspacePath, "content"), { recursive: true }); - writeFileSync(filePath, "# Guide\n"); - - const resolver = new WorkspacePathResolver({ workspacePath }); - - const pathInfo = resolver.resolveExistingWithinWorkspace(filePath); - - assert.equal(pathInfo.path, filePath); - assert.equal(pathInfo.relativePath, "content/guide.md"); - }); - }); - - it("rejects paths outside the workspace", async () => { - await withTempDir(async (tempDir) => { - const workspacePath = join(tempDir, "workspace"); - const externalPath = join(tempDir, "external", "guide.md"); - - mkdirSync(join(tempDir, "external"), { recursive: true }); - mkdirSync(workspacePath, { recursive: true }); - writeFileSync(externalPath, "# Guide\n"); - - const resolver = new WorkspacePathResolver({ workspacePath }); - - assert.throws( - () => resolver.resolveExistingWithinWorkspace(externalPath), - /Path must stay within workspace/, - ); - }); - }); - }); + describe("WorkspacePathResolver.resolveExistingWithinWorkspace", () => { + it("resolves an existing path inside the workspace", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const filePath = join(workspacePath, "content", "guide.md"); + + mkdirSync(join(workspacePath, "content"), { recursive: true }); + writeFileSync(filePath, "# Guide\n"); + + const resolver = new WorkspacePathResolver({ workspacePath }); + + const pathInfo = resolver.resolveExistingWithinWorkspace(filePath); + + assert.equal(pathInfo.path, filePath); + assert.equal(pathInfo.relativePath, "content/guide.md"); + }); + }); + + it("rejects paths outside the workspace", async () => { + await withTempDir(async (tempDir) => { + const workspacePath = join(tempDir, "workspace"); + const externalPath = join(tempDir, "external", "guide.md"); + + mkdirSync(join(tempDir, "external"), { recursive: true }); + mkdirSync(workspacePath, { recursive: true }); + writeFileSync(externalPath, "# Guide\n"); + + const resolver = new WorkspacePathResolver({ workspacePath }); + + assert.throws( + () => resolver.resolveExistingWithinWorkspace(externalPath), + /Path must stay within workspace/, + ); + }); + }); + }); }); diff --git a/actions/deploy/report/action.yml b/actions/deploy/report/action.yml index e01d080..b661fdc 100644 --- a/actions/deploy/report/action.yml +++ b/actions/deploy/report/action.yml @@ -58,7 +58,7 @@ runs: using: "composite" steps: - id: local-actions - uses: hoverkraft-tech/ci-github-common/actions/local-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: source-path: ${{ github.action_path }}/../.. @@ -114,7 +114,7 @@ runs: update-log-url: ${{ inputs.repository == github.event.repository.name && 'true' || 'false' }} github-token: ${{ inputs.github-token }} - - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/create-or-update-comment@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 if: ${{ steps.get-deployment-result.outputs.is-issue-comment == 'true' }} with: title: "## ${{ steps.get-deployment-result.outputs.title }}" @@ -124,10 +124,13 @@ runs: - if: ${{ steps.get-deployment-result.outputs.is-issue-comment == 'true' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_REPOSITORY: ${{ inputs.repository }} + STEP_REACTION: ${{ steps.get-deployment-result.outputs.reaction }} with: github-token: ${{ inputs.github-token }} script: | - const repository = `${{ inputs.repository }}` || context.repo.repo; + const repository = process.env.INPUT_REPOSITORY || context.repo.repo; const issueCommentPayload = { owner: context.repo.owner, @@ -146,5 +149,5 @@ runs: await github.rest.reactions.createForIssueComment({ ...issueCommentPayload, - content: "${{ steps.get-deployment-result.outputs.reaction }}" + content: process.env.STEP_REACTION }); diff --git a/actions/deploy/report/scripts/context.js b/actions/deploy/report/scripts/context.js index fdcf235..9a05138 100644 --- a/actions/deploy/report/scripts/context.js +++ b/actions/deploy/report/scripts/context.js @@ -1,40 +1,40 @@ async function getDeploymentContext({ - github, - context, - environment, - deploymentEnvironment, - url, - eventName, - hasFailed, - failedJobs, - extra, + github, + context, + environment, + deploymentEnvironment, + url, + eventName, + hasFailed, + failedJobs, + extra, }) { - const trimmedDeploymentEnvironment = (deploymentEnvironment || "").trim(); - const trimmedInputEnvironment = (environment || "").trim(); + const trimmedDeploymentEnvironment = (deploymentEnvironment || "").trim(); + const trimmedInputEnvironment = (environment || "").trim(); - const resolvedEnvironment = - trimmedDeploymentEnvironment || trimmedInputEnvironment || ""; - const resolvedUrl = url ? url : null; - const isIssueComment = (eventName || "") === "issue_comment"; + const resolvedEnvironment = + trimmedDeploymentEnvironment || trimmedInputEnvironment || ""; + const resolvedUrl = url ? url : null; + const isIssueComment = (eventName || "") === "issue_comment"; - const { - data: { html_url: htmlUrl }, - } = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId, - }); + const { + data: { html_url: htmlUrl }, + } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId, + }); - return { - environment: resolvedEnvironment, - url: resolvedUrl, - isIssueComment, - htmlUrl, - hasFailed: String(hasFailed || "").toLowerCase() === "true", - failedJobsRaw: failedJobs || "", - extraRaw: extra || "", - workflowName: context.workflow, - }; + return { + environment: resolvedEnvironment, + url: resolvedUrl, + isIssueComment, + htmlUrl, + hasFailed: String(hasFailed || "").toLowerCase() === "true", + failedJobsRaw: failedJobs || "", + extraRaw: extra || "", + workflowName: context.workflow, + }; } module.exports = { getDeploymentContext }; diff --git a/actions/deploy/report/scripts/get-deployment-result.js b/actions/deploy/report/scripts/get-deployment-result.js index 9d744c3..9051f7e 100644 --- a/actions/deploy/report/scripts/get-deployment-result.js +++ b/actions/deploy/report/scripts/get-deployment-result.js @@ -3,61 +3,61 @@ const { buildDeploymentResult } = require("./result"); const { buildSummaryList } = require("./summary"); async function run({ - github, - context, - core, - environment, - deploymentEnvironment, - url, - eventName, - hasFailed, - failedJobs, - extra, + github, + context, + core, + environment, + deploymentEnvironment, + url, + eventName, + hasFailed, + failedJobs, + extra, }) { - const deploymentContext = await getDeploymentContext({ - github, - context, - environment, - deploymentEnvironment, - url, - eventName, - hasFailed, - failedJobs, - extra, - }); - - const deploymentUrl = deploymentContext.url ?? null; - core.setOutput("url", deploymentUrl); - - if (deploymentContext.isIssueComment) { - core.setOutput("is-issue-comment", true); - } - - const result = buildDeploymentResult({ core, ...deploymentContext }); - - core.setOutput("state", result.state); - core.setOutput("title", result.title); - core.setOutput("message", result.message); - core.setOutput("reaction", result.reaction); - - const summaryList = buildSummaryList({ core, ...deploymentContext }); - - await core.summary - .addHeading( - `Deployment summary${ - deploymentContext.environment - ? ` - ${deploymentContext.environment}` - : "" - }`, - 2, - ) - .addRaw(result.title, true) - .addBreak() - .addBreak() - .addRaw(result.message, false) - .addSeparator() - .addList(summaryList) - .write(); + const deploymentContext = await getDeploymentContext({ + github, + context, + environment, + deploymentEnvironment, + url, + eventName, + hasFailed, + failedJobs, + extra, + }); + + const deploymentUrl = deploymentContext.url ?? null; + core.setOutput("url", deploymentUrl); + + if (deploymentContext.isIssueComment) { + core.setOutput("is-issue-comment", true); + } + + const result = buildDeploymentResult({ core, ...deploymentContext }); + + core.setOutput("state", result.state); + core.setOutput("title", result.title); + core.setOutput("message", result.message); + core.setOutput("reaction", result.reaction); + + const summaryList = buildSummaryList({ core, ...deploymentContext }); + + await core.summary + .addHeading( + `Deployment summary${ + deploymentContext.environment + ? ` - ${deploymentContext.environment}` + : "" + }`, + 2, + ) + .addRaw(result.title, true) + .addBreak() + .addBreak() + .addRaw(result.message, false) + .addSeparator() + .addList(summaryList) + .write(); } module.exports = run; diff --git a/actions/deploy/report/scripts/result.js b/actions/deploy/report/scripts/result.js index 2e6f4b1..6d9298a 100644 --- a/actions/deploy/report/scripts/result.js +++ b/actions/deploy/report/scripts/result.js @@ -1,91 +1,91 @@ function parseFailedJobs(rawValue, core) { - if (!rawValue) { - return []; - } - - let parsed; - try { - parsed = JSON.parse(rawValue); - } catch (error) { - core.setFailed(`"failed-jobs" output is not a valid JSON: ${error}`); - return []; - } - - if (!Array.isArray(parsed)) { - core.setFailed('Output "failed-jobs" expected to be a JSON array.'); - return []; - } - - return parsed; + if (!rawValue) { + return []; + } + + let parsed; + try { + parsed = JSON.parse(rawValue); + } catch (error) { + core.setFailed(`"failed-jobs" output is not a valid JSON: ${error}`); + return []; + } + + if (!Array.isArray(parsed)) { + core.setFailed('Output "failed-jobs" expected to be a JSON array.'); + return []; + } + + return parsed; } function buildFailureMessage({ htmlUrl, failedJobs }) { - let message = `The deployment has failed. Please check the logs and try again.`; + let message = `The deployment has failed. Please check the logs and try again.`; - if (failedJobs.length > 0) { - message += "\n\n\n### The following jobs have failed:\n"; + if (failedJobs.length > 0) { + message += "\n\n\n### The following jobs have failed:\n"; - for (const { name, conclusion, html_url: jobUrl } of failedJobs) { - message += `- **${name}**: [${conclusion}](${jobUrl})\n`; - } - } + for (const { name, conclusion, html_url: jobUrl } of failedJobs) { + message += `- **${name}**: [${conclusion}](${jobUrl})\n`; + } + } - return message; + return message; } function buildSuccessMessage({ url, isIssueComment }) { - let message = ""; + let message = ""; - if (url) { - message += `Here it is: ${url}\n\n\`\`\`\n${url}\n\`\`\`\n\n`; - } + if (url) { + message += `Here it is: ${url}\n\n\`\`\`\n${url}\n\`\`\`\n\n`; + } - if (isIssueComment) { - message += - "Once the Pull Request gets merged or closed, the review app will automatically be deleted.\n\n"; - } + if (isIssueComment) { + message += + "Once the Pull Request gets merged or closed, the review app will automatically be deleted.\n\n"; + } - return message; + return message; } function buildDeploymentResult({ - core, - environment, - htmlUrl, - url, - hasFailed, - failedJobsRaw, - isIssueComment, + core, + environment, + htmlUrl, + url, + hasFailed, + failedJobsRaw, + isIssueComment, }) { - let state; - let title; - let message; - let reaction; - - if (hasFailed) { - state = "failure"; - title = `Failed to deploy${ - environment ? ` to ${environment}` : "" - } :confused: !`; - - const failedJobs = parseFailedJobs(failedJobsRaw, core); - message = buildFailureMessage({ htmlUrl, failedJobs }); - reaction = "confused"; - } else { - state = "success"; - title = `Successful deployment${ - environment ? ` to ${environment}` : "" - } :sparkles: !`; - message = buildSuccessMessage({ url, isIssueComment }); - reaction = "rocket"; - } - - return { - state, - title, - message, - reaction, - }; + let state; + let title; + let message; + let reaction; + + if (hasFailed) { + state = "failure"; + title = `Failed to deploy${ + environment ? ` to ${environment}` : "" + } :confused: !`; + + const failedJobs = parseFailedJobs(failedJobsRaw, core); + message = buildFailureMessage({ htmlUrl, failedJobs }); + reaction = "confused"; + } else { + state = "success"; + title = `Successful deployment${ + environment ? ` to ${environment}` : "" + } :sparkles: !`; + message = buildSuccessMessage({ url, isIssueComment }); + reaction = "rocket"; + } + + return { + state, + title, + message, + reaction, + }; } module.exports = { buildDeploymentResult }; diff --git a/actions/deploy/report/scripts/summary.js b/actions/deploy/report/scripts/summary.js index 4c60963..a97b030 100644 --- a/actions/deploy/report/scripts/summary.js +++ b/actions/deploy/report/scripts/summary.js @@ -1,206 +1,206 @@ function formatLabel(key) { - if (!key) { - return ""; - } - - return key - .replace(/([A-Z])/g, " $1") - .replace(/[-_]/g, " ") - .replace(/\s+/g, " ") - .trim() - .replace(/^./, (char) => char.toUpperCase()); + if (!key) { + return ""; + } + + return key + .replace(/([A-Z])/g, " $1") + .replace(/[-_]/g, " ") + .replace(/\s+/g, " ") + .trim() + .replace(/^./, (char) => char.toUpperCase()); } function createSummaryHelper(core) { - const summary = core.summary; - - function withTemporaryBuffer(callback) { - const previousBuffer = summary.stringify(); - summary.emptyBuffer(); - - let result = ""; - - try { - callback(summary); - result = summary.stringify().replace(/\r?\n$/, ""); - } finally { - summary.emptyBuffer(); - summary.addRaw(previousBuffer); - } - - return result; - } - - function list(items, ordered = false) { - const normalizedItems = (Array.isArray(items) ? items : [items]) - .filter((item) => item !== undefined && item !== null) - .map((item) => String(item)); - - if (!normalizedItems.length) { - return "-"; - } - - return withTemporaryBuffer((summaryInstance) => { - summaryInstance.addList(normalizedItems, ordered); - }); - } - - function concat(chunks) { - return withTemporaryBuffer((summaryInstance) => { - for (const chunk of chunks) { - if (chunk === undefined || chunk === null) { - continue; - } - - summaryInstance.addRaw(String(chunk)); - } - }); - } - - function link(text, href) { - if (!href) { - return String(text || "-"); - } - - return withTemporaryBuffer((summaryInstance) => { - summaryInstance.addLink(String(text || href), String(href)); - }); - } - - return { list, concat, link }; + const summary = core.summary; + + function withTemporaryBuffer(callback) { + const previousBuffer = summary.stringify(); + summary.emptyBuffer(); + + let result = ""; + + try { + callback(summary); + result = summary.stringify().replace(/\r?\n$/, ""); + } finally { + summary.emptyBuffer(); + summary.addRaw(previousBuffer); + } + + return result; + } + + function list(items, ordered = false) { + const normalizedItems = (Array.isArray(items) ? items : [items]) + .filter((item) => item !== undefined && item !== null) + .map((item) => String(item)); + + if (!normalizedItems.length) { + return "-"; + } + + return withTemporaryBuffer((summaryInstance) => { + summaryInstance.addList(normalizedItems, ordered); + }); + } + + function concat(chunks) { + return withTemporaryBuffer((summaryInstance) => { + for (const chunk of chunks) { + if (chunk === undefined || chunk === null) { + continue; + } + + summaryInstance.addRaw(String(chunk)); + } + }); + } + + function link(text, href) { + if (!href) { + return String(text || "-"); + } + + return withTemporaryBuffer((summaryInstance) => { + summaryInstance.addLink(String(text || href), String(href)); + }); + } + + return { list, concat, link }; } function getSummaryHelpers(core) { - const helpers = createSummaryHelper(core); + const helpers = createSummaryHelper(core); - if (!helpers) { - const message = - "core.summary is not available or missing required methods."; + if (!helpers) { + const message = + "core.summary is not available or missing required methods."; - return core.setFailed(message); - } + return core.setFailed(message); + } - return helpers; + return helpers; } function isPlainObject(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - return false; - } + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false; + } - const prototype = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; } function formatScalar(value) { - if (value === undefined || value === null || value === "") { - return "-"; - } + if (value === undefined || value === null || value === "") { + return "-"; + } - if (typeof value === "boolean") { - return value ? "true" : "false"; - } + if (typeof value === "boolean") { + return value ? "true" : "false"; + } - return String(value); + return String(value); } function formatNestedItem(label, value, helpers) { - const formattedValue = formatValue(value, helpers); - if (!label) { - return formattedValue; - } + const formattedValue = formatValue(value, helpers); + if (!label) { + return formattedValue; + } - return helpers.concat([`${label}: `, formattedValue]); + return helpers.concat([`${label}: `, formattedValue]); } function formatArray(items, helpers) { - if (!items.length) { - return "-"; - } + if (!items.length) { + return "-"; + } - const listItems = items.map((item, index) => - formatNestedItem(`Item ${index + 1}`, item, helpers), - ); + const listItems = items.map((item, index) => + formatNestedItem(`Item ${index + 1}`, item, helpers), + ); - return helpers.list(listItems); + return helpers.list(listItems); } function formatObject(value, helpers) { - const entries = Object.entries(value); + const entries = Object.entries(value); - if (!entries.length) { - return "-"; - } + if (!entries.length) { + return "-"; + } - const listItems = entries.map(([childKey, childValue]) => - formatNestedItem(formatLabel(childKey), childValue, helpers), - ); + const listItems = entries.map(([childKey, childValue]) => + formatNestedItem(formatLabel(childKey), childValue, helpers), + ); - return helpers.list(listItems); + return helpers.list(listItems); } function formatValue(value, helpers) { - if (Array.isArray(value)) { - return formatArray(value, helpers); - } + if (Array.isArray(value)) { + return formatArray(value, helpers); + } - if (isPlainObject(value)) { - return formatObject(value, helpers); - } + if (isPlainObject(value)) { + return formatObject(value, helpers); + } - return formatScalar(value); + return formatScalar(value); } function buildSummaryItem(key, value, helpers) { - return formatNestedItem(formatLabel(key), value, helpers); + return formatNestedItem(formatLabel(key), value, helpers); } function parseExtra(extraRaw, core) { - if (!extraRaw) { - return null; - } - - let parsed; - try { - parsed = JSON.parse(extraRaw); - } catch (error) { - core.setFailed(`"extra" input is not a valid JSON: ${error}`); - return null; - } - - if (!parsed || typeof parsed !== "object") { - core.warning('"extra" input is not a valid JSON object.'); - return null; - } - - return parsed; + if (!extraRaw) { + return null; + } + + let parsed; + try { + parsed = JSON.parse(extraRaw); + } catch (error) { + core.setFailed(`"extra" input is not a valid JSON: ${error}`); + return null; + } + + if (!parsed || typeof parsed !== "object") { + core.warning('"extra" input is not a valid JSON object.'); + return null; + } + + return parsed; } function buildSummaryList({ - core, - environment, - htmlUrl, - workflowName, - extraRaw, + core, + environment, + htmlUrl, + workflowName, + extraRaw, }) { - const helpers = getSummaryHelpers(core); + const helpers = getSummaryHelpers(core); - const summaryList = [ - helpers.concat(["Logs: ", helpers.link(workflowName, htmlUrl)]), - ]; + const summaryList = [ + helpers.concat(["Logs: ", helpers.link(workflowName, htmlUrl)]), + ]; - if (environment) { - summaryList.unshift(helpers.concat(["Environment: ", environment])); - } + if (environment) { + summaryList.unshift(helpers.concat(["Environment: ", environment])); + } - const extra = parseExtra(extraRaw, core); - if (extra) { - for (const [key, value] of Object.entries(extra)) { - summaryList.push(buildSummaryItem(key, value, helpers)); - } - } + const extra = parseExtra(extraRaw, core); + if (extra) { + for (const [key, value] of Object.entries(extra)) { + summaryList.push(buildSummaryItem(key, value, helpers)); + } + } - return summaryList; + return summaryList; } module.exports = { buildSummaryList }; diff --git a/actions/deployment/get-finished/action.yml b/actions/deployment/get-finished/action.yml index a88e6d3..5553ef6 100644 --- a/actions/deployment/get-finished/action.yml +++ b/actions/deployment/get-finished/action.yml @@ -45,12 +45,16 @@ runs: steps: - id: get-deployment-status uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_DEPLOYMENT_ID: ${{ inputs.deployment-id }} + INPUT_TIMEOUT: ${{ inputs.timeout }} + INPUT_ALLOW_FAILURE: ${{ inputs.allow-failure }} with: script: | - const deploymentId = `${{ inputs.deployment-id }}`; + const deploymentId = process.env.INPUT_DEPLOYMENT_ID; const finishedStatuses = ["success", "failure", "error"]; - const timeout = parseInt(`${{ inputs.timeout }}`, 10); + const timeout = parseInt(process.env.INPUT_TIMEOUT, 10); if (isNaN(timeout) || timeout <= 0) { core.setFailed(`Invalid timeout value: ${timeout}`); return; @@ -112,7 +116,7 @@ runs: core.setOutput("status", deploymentStatus.state); core.setOutput("environment", deploymentStatus.environment); - const allowFailure = `${{ inputs.allow-failure }}`.toLowerCase() === "true"; + const allowFailure = process.env.INPUT_ALLOW_FAILURE.toLowerCase() === "true"; if (!allowFailure && deploymentStatus.state !== "success") { core.setFailed(`Deployment ${deploymentId} failed with status: ${deploymentStatus.state}`); } diff --git a/actions/deployment/read/action.yml b/actions/deployment/read/action.yml index cb0f597..98aff75 100644 --- a/actions/deployment/read/action.yml +++ b/actions/deployment/read/action.yml @@ -35,19 +35,22 @@ runs: steps: - id: get-deployment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_DEPLOYMENT_ID: ${{ inputs.deployment-id }} + INPUT_REPOSITORY: ${{ inputs.repository }} with: github-token: ${{ inputs.github-token }} script: | - const repository = `${{ inputs.repository }}`.trim(); + const repository = process.env.INPUT_REPOSITORY || context.repo.repo; const { data: deployment } = await github.rest.repos.getDeployment({ owner: context.repo.owner, repo: repository, - deployment_id: `${{ inputs.deployment-id }}` + deployment_id: process.env.INPUT_DEPLOYMENT_ID }); if (!deployment) { - core.setFailed(`Deployment with id "${{ inputs.deployment-id }}" not found in repository "${repository}"`); + core.setFailed(`Deployment with id "${process.env.INPUT_DEPLOYMENT_ID}" not found in repository "${repository}"`); return; } diff --git a/actions/deployment/update/action.yml b/actions/deployment/update/action.yml index d461ea2..94550a4 100644 --- a/actions/deployment/update/action.yml +++ b/actions/deployment/update/action.yml @@ -40,15 +40,21 @@ runs: steps: - id: create-deployment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_DEPLOYMENT_ID: ${{ inputs.deployment-id }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_STATE: ${{ inputs.state }} + INPUT_URL: ${{ inputs.url }} + INPUT_UPDATE_LOG_URL: ${{ inputs.update-log-url }} with: github-token: ${{ inputs.github-token }} script: | - const repository = `${{ inputs.repository }}`.trim(); + const repository = process.env.INPUT_REPOSITORY || context.repo.repo; let logUrl = null; - const shouldUpdateLogUrl = `${{ inputs.update-log-url }}`.trim(); - if (shouldUpdateLogUrl === "true") { + const shouldUpdateLogUrl = process.env.INPUT_UPDATE_LOG_URL.toLowerCase() === "true"; + if (shouldUpdateLogUrl) { const { data: { html_url } } = await github.rest.actions.getWorkflowRun({ owner: context.repo.owner, repo: repository, @@ -61,9 +67,9 @@ runs: await github.rest.repos.createDeploymentStatus({ owner: context.repo.owner, repo: repository, - deployment_id: `${{ inputs.deployment-id }}`, - state: `${{ inputs.state }}`, - environment_url: `${{ inputs.url }}`, + deployment_id: process.env.INPUT_DEPLOYMENT_ID, + state: process.env.INPUT_STATE, + environment_url: process.env.INPUT_URL, log_url: logUrl ?? undefined, auto_inactive: true, }); diff --git a/actions/release/create/action.yml b/actions/release/create/action.yml index 8c0e36c..17d7393 100644 --- a/actions/release/create/action.yml +++ b/actions/release/create/action.yml @@ -16,23 +16,19 @@ inputs: Working directory used to scope release automation in a monorepo. If specified, the action looks for `.github/release-configs/{slug}.yml`, where `slug` is derived from the working directory basename. If that file does not exist, a temporary release configuration is generated with `include-paths` for the working directory and current workflow file. - type: string required: false default: "" include-paths: description: | Additional paths to include in the release notes filtering (JSON array). These paths are added to the `include-paths` configuration of release-drafter. - type: string required: false default: "[]" tag: description: "Release tag name to use in explicit mode" required: false target-sha: - description: | - Optional commit SHA or branch name to target when explicit mode creates a release for a tag that does not already exist. - Forwarded to Release Drafter as `commitish`. + description: "Optional commit SHA or branch name to target when explicit mode creates a release for a tag that does not already exist. Forwarded to Release Drafter as `commitish`." # codespell:ignore commitish required: false default: "" github-token: @@ -180,7 +176,7 @@ runs: core.setOutput('summary-template', summaryTemplate); - id: local-actions - uses: hoverkraft-tech/ci-github-common/actions/local-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: source-path: ${{ github.action_path }}/.. @@ -483,7 +479,7 @@ runs: with: token: ${{ inputs.github-token }} tag: ${{ steps.resolve-release-inputs.outputs.explicit-tag }} - commitish: ${{ steps.resolve-explicit-release-inputs.outputs.commitish }} + commitish: ${{ steps.resolve-explicit-release-inputs.outputs.commitish }} # codespell:ignore commitish publish: "false" # config-path is relative to ".github" directory config-name: file:${{ steps.get-configuration.outputs.config-path }} diff --git a/actions/release/get-configuration/action.yml b/actions/release/get-configuration/action.yml index 2d70637..d7f3646 100644 --- a/actions/release/get-configuration/action.yml +++ b/actions/release/get-configuration/action.yml @@ -11,14 +11,12 @@ inputs: Working directory used to scope release automation in a monorepo. If specified, the action looks for `.github/release-configs/{slug}.yml`, where `slug` is derived from the working directory basename. If that file does not exist, a temporary release configuration is generated with `include-paths` for the working directory and current workflow file. - type: string required: false default: "" include-paths: description: | Additional paths to include in the release notes filtering (JSON array). These paths are added to the `include-paths` configuration of release-drafter. - type: string required: false default: "[]" @@ -74,7 +72,7 @@ runs: core.setOutput('temp-config-path', tempConfigRelativePath); - if: steps.resolve-config-paths.outputs.requires-checkout == 'true' - uses: hoverkraft-tech/ci-github-common/actions/checkout@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/checkout@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: fetch-depth: "1" sparse-checkout: .github/${{ steps.resolve-config-paths.outputs.repository-config-path }} diff --git a/actions/release/plan/action.yml b/actions/release/plan/action.yml index 814b2e1..514dd07 100644 --- a/actions/release/plan/action.yml +++ b/actions/release/plan/action.yml @@ -15,14 +15,12 @@ inputs: Working directory used to scope release automation in a monorepo. If specified, the action looks for `.github/release-configs/{slug}.yml`, where `slug` is derived from the working directory basename. If that file does not exist, a temporary release configuration is generated with `include-paths` for the working directory and current workflow file. - type: string required: false default: "" include-paths: description: | Additional paths to include in the release notes filtering (JSON array). These paths are added to the `include-paths` configuration of release-drafter. - type: string required: false default: "[]" github-token: @@ -45,12 +43,12 @@ outputs: runs: using: "composite" steps: - - uses: hoverkraft-tech/ci-github-common/actions/checkout@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + - uses: hoverkraft-tech/ci-github-common/actions/checkout@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: fetch-depth: "0" - id: local-actions - uses: hoverkraft-tech/ci-github-common/actions/local-actions@b553a696531fbd36743ccbb0c76c717971b8acdb # 0.35.4 + uses: hoverkraft-tech/ci-github-common/actions/local-actions@6718ae98e8b6e009f8f2790af074daa1a06946c2 # 0.36.2 with: source-path: ${{ github.action_path }}/.. diff --git a/actions/release/summarize-changelog/action.yml b/actions/release/summarize-changelog/action.yml index ff6a46a..dbcd079 100644 --- a/actions/release/summarize-changelog/action.yml +++ b/actions/release/summarize-changelog/action.yml @@ -54,7 +54,7 @@ runs: using: "composite" steps: - name: Setup Node.js - uses: hoverkraft-tech/ci-github-nodejs/actions/setup-node@6b74a8f070140f5c120f78026d58e4c00d1b1e37 # 0.24.2 + uses: hoverkraft-tech/ci-github-nodejs/actions/setup-node@df348077afa4e79725151d50606e9dc63f86dcb6 # 0.24.4 with: working-directory: ${{ github.action_path }} diff --git a/actions/release/summarize-changelog/package-lock.json b/actions/release/summarize-changelog/package-lock.json index 58cc3a0..8a52be7 100644 --- a/actions/release/summarize-changelog/package-lock.json +++ b/actions/release/summarize-changelog/package-lock.json @@ -1,692 +1,692 @@ { - "name": "release-summarize-changelog", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "release-summarize-changelog", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@langchain/anthropic": "1.4.0", - "@langchain/google-genai": "2.1.31", - "@langchain/openai": "1.4.7", - "html-to-text": "^10.0.0", - "langchain": "1.4.4" - }, - "engines": { - "node": ">=24.0.0" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.95.2", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.2.tgz", - "integrity": "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1", - "standardwebhooks": "^1.0.0" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cfworker/json-schema": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", - "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT", - "peer": true - }, - "node_modules/@google/generative-ai": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", - "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@langchain/anthropic": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.0.tgz", - "integrity": "sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.95.1", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.47" - } - }, - "node_modules/@langchain/core": { - "version": "1.1.48", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.48.tgz", - "integrity": "sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@cfworker/json-schema": "^4.0.2", - "@standard-schema/spec": "^1.1.0", - "js-tiktoken": "^1.0.12", - "langsmith": ">=0.5.0 <1.0.0", - "mustache": "^4.2.0", - "p-queue": "^6.6.2", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@langchain/google-genai": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-2.1.31.tgz", - "integrity": "sha512-lHIJGtZab0jqoufKRPXyHHg1nLXrE74LXd0ftgibWEACc1SpSLu6XwtA23+dX4l7Q/YeSgb9n40YJx5k00/fqw==", - "license": "MIT", - "dependencies": { - "@google/generative-ai": "^0.24.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.47" - } - }, - "node_modules/@langchain/langgraph": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.3.3.tgz", - "integrity": "sha512-8xbpGUQNBcWua7ivT5vUvDnQ+6Qbt0JO8RisgXZ8guPXNqh8plGVvrODW68S4AlJbOYY2yi0ROKtrL/1yN3MBQ==", - "license": "MIT", - "dependencies": { - "@langchain/langgraph-checkpoint": "^1.0.4", - "@langchain/langgraph-sdk": "~1.9.11", - "@langchain/protocol": "^0.0.16", - "@standard-schema/spec": "1.1.0", - "uuid": "^14.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": "^1.1.44", - "zod": "^3.25.32 || ^4.2.0", - "zod-to-json-schema": "^3.x" - }, - "peerDependenciesMeta": { - "zod-to-json-schema": { - "optional": true - } - } - }, - "node_modules/@langchain/langgraph-checkpoint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.4.tgz", - "integrity": "sha512-1y5MgZ0gXXrtmoy56e3kaBChI3GwFPIKl27xkrHwN+VE/3iUsyr9gO3Jtp7kdKAe6diZGbcas5bdC/r0yUwTZA==", - "license": "MIT", - "dependencies": { - "uuid": "^14.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": "^1.1.44" - } - }, - "node_modules/@langchain/langgraph-sdk": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.11.tgz", - "integrity": "sha512-mhadkZy4LQ97NJwvATiVIkSxVfOnauXNhrVHFgGnzyqr5zzPLS0VIKJW9xKT+pM8yLqW8Qj6+nPPNhwGUaxoRw==", - "license": "MIT", - "dependencies": { - "@langchain/protocol": "^0.0.16", - "@types/json-schema": "^7.0.15", - "p-queue": "^9.0.1", - "p-retry": "^7.1.1", - "uuid": "^14.0.0" - }, - "peerDependencies": { - "@langchain/core": "^1.1.44", - "react": "^18 || ^19", - "react-dom": "^18 || ^19", - "svelte": "^4.0.0 || ^5.0.0", - "vue": "^3.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@langchain/openai": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.4.7.tgz", - "integrity": "sha512-i1YLV4pWbGC6W8m0ZNpLObJuf1nyU4o8aWyX4AF9fHn7eM67HfIJWQ5n5XzcCpuSa41otrxA9jvH5XRKwI1qDA==", - "license": "MIT", - "dependencies": { - "js-tiktoken": "^1.0.12", - "openai": "^6.37.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.48" - } - }, - "node_modules/@langchain/protocol": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.16.tgz", - "integrity": "sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==", - "license": "MIT" - }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.12.0.tgz", - "integrity": "sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==", - "license": "MIT", - "dependencies": { - "domelementtype": "~2.3.0", - "domhandler": "~5.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - }, - "peerDependencies": { - "selderee": "~0.12.0" - } - }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, - "node_modules/html-to-text": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.0.tgz", - "integrity": "sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==", - "license": "MIT", - "dependencies": { - "@selderee/plugin-htmlparser2": "~0.12.0", - "deepmerge-ts": "^7.1.5", - "dom-serializer": "^2.0.0", - "htmlparser2": "^10.1.0", - "selderee": "~0.12.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - } - }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/is-network-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", - "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-tiktoken": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", - "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.5.1" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/langchain": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.4.4.tgz", - "integrity": "sha512-tepOCwUDaIZOYJ9Eo0O6o5dXEN/0KJheiFDnHHFL8Tx8rfkDLL4cOTSTln4Vpn9LpWzXYkjQ8lkHnnNDQWZPeg==", - "license": "MIT", - "dependencies": { - "@langchain/langgraph": "^1.3.2", - "@langchain/langgraph-checkpoint": "^1.0.1", - "langsmith": ">=0.5.0 <1.0.0", - "zod": "^3.25.76 || ^4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@langchain/core": "^1.1.48" - } - }, - "node_modules/langsmith": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.7.1.tgz", - "integrity": "sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==", - "license": "MIT", - "dependencies": { - "p-queue": "6.6.2" - }, - "peerDependencies": { - "@opentelemetry/api": "*", - "@opentelemetry/exporter-trace-otlp-proto": "*", - "@opentelemetry/sdk-trace-base": "*", - "openai": "*", - "ws": ">=7" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-proto": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - }, - "openai": { - "optional": true - }, - "ws": { - "optional": true - } - } - }, - "node_modules/leac": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.7.0.tgz", - "integrity": "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - } - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", - "peer": true, - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/openai": { - "version": "6.38.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.38.0.tgz", - "integrity": "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", - "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", - "license": "MIT", - "dependencies": { - "is-network-error": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parseley": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.13.1.tgz", - "integrity": "sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==", - "license": "MIT", - "dependencies": { - "leac": "^0.7.0", - "peberminta": "^0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - } - }, - "node_modules/peberminta": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.10.0.tgz", - "integrity": "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - } - }, - "node_modules/selderee": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.12.0.tgz", - "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", - "license": "MIT", - "dependencies": { - "parseley": "~0.13.1" - }, - "funding": { - "url": "https://github.com/sponsors/KillyMXI" - } - }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } + "name": "release-summarize-changelog", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "release-summarize-changelog", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@langchain/anthropic": "1.4.0", + "@langchain/google-genai": "2.1.31", + "@langchain/openai": "1.4.7", + "html-to-text": "^10.0.0", + "langchain": "1.4.4" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.95.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.2.tgz", + "integrity": "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT", + "peer": true + }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@langchain/anthropic": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.0.tgz", + "integrity": "sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.95.1", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.47" + } + }, + "node_modules/@langchain/core": { + "version": "1.1.48", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.48.tgz", + "integrity": "sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/google-genai": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-2.1.31.tgz", + "integrity": "sha512-lHIJGtZab0jqoufKRPXyHHg1nLXrE74LXd0ftgibWEACc1SpSLu6XwtA23+dX4l7Q/YeSgb9n40YJx5k00/fqw==", + "license": "MIT", + "dependencies": { + "@google/generative-ai": "^0.24.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.47" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.3.3.tgz", + "integrity": "sha512-8xbpGUQNBcWua7ivT5vUvDnQ+6Qbt0JO8RisgXZ8guPXNqh8plGVvrODW68S4AlJbOYY2yi0ROKtrL/1yN3MBQ==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.4", + "@langchain/langgraph-sdk": "~1.9.11", + "@langchain/protocol": "^0.0.16", + "@standard-schema/spec": "1.1.0", + "uuid": "^14.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.44", + "zod": "^3.25.32 || ^4.2.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.4.tgz", + "integrity": "sha512-1y5MgZ0gXXrtmoy56e3kaBChI3GwFPIKl27xkrHwN+VE/3iUsyr9gO3Jtp7kdKAe6diZGbcas5bdC/r0yUwTZA==", + "license": "MIT", + "dependencies": { + "uuid": "^14.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.44" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.9.11", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.11.tgz", + "integrity": "sha512-mhadkZy4LQ97NJwvATiVIkSxVfOnauXNhrVHFgGnzyqr5zzPLS0VIKJW9xKT+pM8yLqW8Qj6+nPPNhwGUaxoRw==", + "license": "MIT", + "dependencies": { + "@langchain/protocol": "^0.0.16", + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1", + "uuid": "^14.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.1.44", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "svelte": "^4.0.0 || ^5.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/openai": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.4.7.tgz", + "integrity": "sha512-i1YLV4pWbGC6W8m0ZNpLObJuf1nyU4o8aWyX4AF9fHn7eM67HfIJWQ5n5XzcCpuSa41otrxA9jvH5XRKwI1qDA==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^6.37.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, + "node_modules/@langchain/protocol": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.16.tgz", + "integrity": "sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.12.0.tgz", + "integrity": "sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==", + "license": "MIT", + "dependencies": { + "domelementtype": "~2.3.0", + "domhandler": "~5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + }, + "peerDependencies": { + "selderee": "~0.12.0" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/html-to-text": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.0.tgz", + "integrity": "sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "~0.12.0", + "deepmerge-ts": "^7.1.5", + "dom-serializer": "^2.0.0", + "htmlparser2": "^10.1.0", + "selderee": "~0.12.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/langchain": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.4.4.tgz", + "integrity": "sha512-tepOCwUDaIZOYJ9Eo0O6o5dXEN/0KJheiFDnHHFL8Tx8rfkDLL4cOTSTln4Vpn9LpWzXYkjQ8lkHnnNDQWZPeg==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph": "^1.3.2", + "@langchain/langgraph-checkpoint": "^1.0.1", + "langsmith": ">=0.5.0 <1.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, + "node_modules/langsmith": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.7.1.tgz", + "integrity": "sha512-Wjk90UjNoY5cBHMlNAC/eZx5clI8jnjBOBW8uJu8+MWBtx0QesNjsUiLtjI+I3UnrpxFFpDqGXcnhBjH654Mqg==", + "license": "MIT", + "dependencies": { + "p-queue": "6.6.2" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*", + "ws": ">=7" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/leac": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.7.0.tgz", + "integrity": "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/openai": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.38.0.tgz", + "integrity": "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parseley": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.13.1.tgz", + "integrity": "sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==", + "license": "MIT", + "dependencies": { + "leac": "^0.7.0", + "peberminta": "^0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/peberminta": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.10.0.tgz", + "integrity": "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/selderee": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.12.0.tgz", + "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", + "license": "MIT", + "dependencies": { + "parseley": "~0.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } } diff --git a/actions/release/summarize-changelog/package.json b/actions/release/summarize-changelog/package.json index 5975ef8..81c5b79 100644 --- a/actions/release/summarize-changelog/package.json +++ b/actions/release/summarize-changelog/package.json @@ -1,31 +1,31 @@ { - "name": "release-summarize-changelog", - "version": "1.0.0", - "description": "Generate concise release summaries from existing changelogs", - "type": "module", - "scripts": { - "test": "node --test", - "test:watch": "node --test --watch", - "test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test" - }, - "keywords": [ - "github-action", - "release", - "changelog", - "summary", - "langchain", - "llm" - ], - "author": "hoverkraft", - "license": "MIT", - "dependencies": { - "@langchain/anthropic": "1.4.0", - "@langchain/google-genai": "2.1.31", - "@langchain/openai": "1.4.7", - "html-to-text": "^10.0.0", - "langchain": "1.4.4" - }, - "engines": { - "node": ">=24.0.0" - } + "name": "release-summarize-changelog", + "version": "1.0.0", + "description": "Generate concise release summaries from existing changelogs", + "type": "module", + "scripts": { + "test": "node --test", + "test:watch": "node --test --watch", + "test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test" + }, + "keywords": [ + "github-action", + "release", + "changelog", + "summary", + "langchain", + "llm" + ], + "author": "hoverkraft", + "license": "MIT", + "dependencies": { + "@langchain/anthropic": "1.4.0", + "@langchain/google-genai": "2.1.31", + "@langchain/openai": "1.4.7", + "html-to-text": "^10.0.0", + "langchain": "1.4.4" + }, + "engines": { + "node": ">=24.0.0" + } } diff --git a/actions/release/summarize-changelog/src/FileSystemService.js b/actions/release/summarize-changelog/src/FileSystemService.js index 6f04207..3ae5ff4 100644 --- a/actions/release/summarize-changelog/src/FileSystemService.js +++ b/actions/release/summarize-changelog/src/FileSystemService.js @@ -3,31 +3,31 @@ import path from "node:path"; const CURRENT_DIRECTORY = "."; export class FileSystemService { - constructor(workingDirectory) { - this.workingDirectory = this.#normalizeRepositoryPath(workingDirectory); - } + constructor(workingDirectory) { + this.workingDirectory = this.#normalizeRepositoryPath(workingDirectory); + } - getRelativeWorkingDirectory() { - return this.workingDirectory; - } + getRelativeWorkingDirectory() { + return this.workingDirectory; + } - #normalizeRepositoryPath(workingDirectory) { - const rawPath = - (workingDirectory || CURRENT_DIRECTORY).trim() || CURRENT_DIRECTORY; - const normalizedPath = path.posix.normalize(rawPath.replace(/\\/g, "/")); + #normalizeRepositoryPath(workingDirectory) { + const rawPath = + (workingDirectory || CURRENT_DIRECTORY).trim() || CURRENT_DIRECTORY; + const normalizedPath = path.posix.normalize(rawPath.replace(/\\/g, "/")); - if ( - path.posix.isAbsolute(normalizedPath) || - normalizedPath === ".." || - normalizedPath.startsWith("../") - ) { - throw new Error( - "The working-directory input must stay within the repository.", - ); - } + if ( + path.posix.isAbsolute(normalizedPath) || + normalizedPath === ".." || + normalizedPath.startsWith("../") + ) { + throw new Error( + "The working-directory input must stay within the repository.", + ); + } - return normalizedPath === CURRENT_DIRECTORY - ? CURRENT_DIRECTORY - : normalizedPath.replace(/\/$/, ""); - } + return normalizedPath === CURRENT_DIRECTORY + ? CURRENT_DIRECTORY + : normalizedPath.replace(/\/$/, ""); + } } diff --git a/actions/release/summarize-changelog/src/GitEvidenceService.js b/actions/release/summarize-changelog/src/GitEvidenceService.js index a4056a5..164950e 100644 --- a/actions/release/summarize-changelog/src/GitEvidenceService.js +++ b/actions/release/summarize-changelog/src/GitEvidenceService.js @@ -3,134 +3,134 @@ const MAX_EVIDENCE_LENGTH = 1500; const MAX_FILE_LINES = 20; export class GitEvidenceService { - constructor(referenceExtractor, logger, githubClient, repositoryContext) { - this.referenceExtractor = referenceExtractor; - this.logger = logger; - this.githubClient = githubClient; - this.repositoryContext = repositoryContext; - } - - async collect(changelogBody, fileSystemService) { - const references = this.referenceExtractor.extract(changelogBody); - const workingDirectory = fileSystemService.getRelativeWorkingDirectory(); - const evidenceBlocks = []; - - for (const sha of references.commitShas.slice(0, MAX_REFERENCES)) { - const block = await this.#getCommitEvidence(sha, workingDirectory); - - if (block) { - evidenceBlocks.push( - `Commit ${sha}:\n${this.#limitText(block, MAX_EVIDENCE_LENGTH)}`, - ); - } - } - - for (const prNumber of references.pullRequests.slice(0, MAX_REFERENCES)) { - const block = await this.#getPullRequestEvidence( - prNumber, - workingDirectory, - ); - - if (block) { - evidenceBlocks.push( - `Pull request #${prNumber}:\n${this.#limitText(block, MAX_EVIDENCE_LENGTH)}`, - ); - } - } - - if (evidenceBlocks.length > 0) { - this.logger.info( - `Collected ${evidenceBlocks.length} git evidence block(s)`, - ); - } - - return evidenceBlocks.join("\n\n").trim(); - } - - async #getCommitEvidence(sha, workingDirectory) { - try { - const response = await this.githubClient.rest.repos.getCommit({ - ...this.repositoryContext, - ref: sha, - }); - const relevantFiles = this.#filterFiles( - response.data.files || [], - workingDirectory, - ); - if (relevantFiles.length === 0) { - return ""; - } - - return [ - response.data.sha, - response.data.commit.message, - "Files:", - ...relevantFiles - .slice(0, MAX_FILE_LINES) - .map((file) => `- ${file.filename}`), - ].join("\n"); - } catch { - return ""; - } - } - - async #getPullRequestEvidence(prNumber, workingDirectory) { - try { - const pullRequest = await this.githubClient.rest.pulls.get({ - ...this.repositoryContext, - pull_number: Number(prNumber), - }); - const files = await this.githubClient.paginate( - this.githubClient.rest.pulls.listFiles, - { - ...this.repositoryContext, - pull_number: Number(prNumber), - per_page: 100, - }, - ); - const relevantFiles = this.#filterFiles(files, workingDirectory); - if (relevantFiles.length === 0) { - return ""; - } - - return [ - `#${pullRequest.data.number} ${pullRequest.data.title}`, - pullRequest.data.body || "", - "Files:", - ...relevantFiles - .slice(0, MAX_FILE_LINES) - .map((file) => `- ${file.filename}`), - ] - .filter(Boolean) - .join("\n"); - } catch { - return ""; - } - } - - #filterFiles(files, workingDirectory) { - if (!Array.isArray(files) || files.length === 0) { - return []; - } - - if (!workingDirectory || workingDirectory === ".") { - return files; - } - - const normalizedPrefix = `${workingDirectory.replace(/\\/g, "/").replace(/\/$/, "")}/`; - return files.filter((file) => { - const filename = file?.filename; - return ( - filename === workingDirectory || filename?.startsWith(normalizedPrefix) - ); - }); - } - - #limitText(text, maxLength) { - if (text.length <= maxLength) { - return text; - } - - return `${text.slice(0, maxLength - 1).trimEnd()}…`; - } + constructor(referenceExtractor, logger, githubClient, repositoryContext) { + this.referenceExtractor = referenceExtractor; + this.logger = logger; + this.githubClient = githubClient; + this.repositoryContext = repositoryContext; + } + + async collect(changelogBody, fileSystemService) { + const references = this.referenceExtractor.extract(changelogBody); + const workingDirectory = fileSystemService.getRelativeWorkingDirectory(); + const evidenceBlocks = []; + + for (const sha of references.commitShas.slice(0, MAX_REFERENCES)) { + const block = await this.#getCommitEvidence(sha, workingDirectory); + + if (block) { + evidenceBlocks.push( + `Commit ${sha}:\n${this.#limitText(block, MAX_EVIDENCE_LENGTH)}`, + ); + } + } + + for (const prNumber of references.pullRequests.slice(0, MAX_REFERENCES)) { + const block = await this.#getPullRequestEvidence( + prNumber, + workingDirectory, + ); + + if (block) { + evidenceBlocks.push( + `Pull request #${prNumber}:\n${this.#limitText(block, MAX_EVIDENCE_LENGTH)}`, + ); + } + } + + if (evidenceBlocks.length > 0) { + this.logger.info( + `Collected ${evidenceBlocks.length} git evidence block(s)`, + ); + } + + return evidenceBlocks.join("\n\n").trim(); + } + + async #getCommitEvidence(sha, workingDirectory) { + try { + const response = await this.githubClient.rest.repos.getCommit({ + ...this.repositoryContext, + ref: sha, + }); + const relevantFiles = this.#filterFiles( + response.data.files || [], + workingDirectory, + ); + if (relevantFiles.length === 0) { + return ""; + } + + return [ + response.data.sha, + response.data.commit.message, + "Files:", + ...relevantFiles + .slice(0, MAX_FILE_LINES) + .map((file) => `- ${file.filename}`), + ].join("\n"); + } catch { + return ""; + } + } + + async #getPullRequestEvidence(prNumber, workingDirectory) { + try { + const pullRequest = await this.githubClient.rest.pulls.get({ + ...this.repositoryContext, + pull_number: Number(prNumber), + }); + const files = await this.githubClient.paginate( + this.githubClient.rest.pulls.listFiles, + { + ...this.repositoryContext, + pull_number: Number(prNumber), + per_page: 100, + }, + ); + const relevantFiles = this.#filterFiles(files, workingDirectory); + if (relevantFiles.length === 0) { + return ""; + } + + return [ + `#${pullRequest.data.number} ${pullRequest.data.title}`, + pullRequest.data.body || "", + "Files:", + ...relevantFiles + .slice(0, MAX_FILE_LINES) + .map((file) => `- ${file.filename}`), + ] + .filter(Boolean) + .join("\n"); + } catch { + return ""; + } + } + + #filterFiles(files, workingDirectory) { + if (!Array.isArray(files) || files.length === 0) { + return []; + } + + if (!workingDirectory || workingDirectory === ".") { + return files; + } + + const normalizedPrefix = `${workingDirectory.replace(/\\/g, "/").replace(/\/$/, "")}/`; + return files.filter((file) => { + const filename = file?.filename; + return ( + filename === workingDirectory || filename?.startsWith(normalizedPrefix) + ); + }); + } + + #limitText(text, maxLength) { + if (text.length <= maxLength) { + return text; + } + + return `${text.slice(0, maxLength - 1).trimEnd()}…`; + } } diff --git a/actions/release/summarize-changelog/src/GitEvidenceService.test.js b/actions/release/summarize-changelog/src/GitEvidenceService.test.js index 991acb0..2a0b9f1 100644 --- a/actions/release/summarize-changelog/src/GitEvidenceService.test.js +++ b/actions/release/summarize-changelog/src/GitEvidenceService.test.js @@ -4,129 +4,129 @@ import { GitEvidenceService } from "./GitEvidenceService.js"; import { ReferenceExtractor } from "./ReferenceExtractor.js"; function createLoggerStub() { - return { - info() {}, - warning() {}, - }; + return { + info() {}, + warning() {}, + }; } describe("GitEvidenceService", () => { - it("collects commit and pull request evidence through GitHub APIs", async () => { - const calls = []; - const service = new GitEvidenceService( - new ReferenceExtractor(), - createLoggerStub(), - { - rest: { - repos: { - async getCommit(parameters) { - calls.push(["getCommit", parameters]); - return { - data: { - sha: "1234567", - commit: { - message: "fix(ui): correct status badge", - }, - files: [ - { - filename: - "actions/release/summarize-changelog/src/index.js", - }, - { filename: "README.md" }, - ], - }, - }; - }, - }, - pulls: { - async get(parameters) { - calls.push(["getPull", parameters]); - return { - data: { - number: 42, - title: "feat(api): add project filters", - body: "Adds filters for technical users.", - }, - }; - }, - listFiles: {}, - }, - }, - async paginate(_method, parameters) { - calls.push(["paginate", parameters]); - return [ - { filename: "actions/release/summarize-changelog/action.yml" }, - { filename: "docs/guide.md" }, - ]; - }, - }, - { owner: "hoverkraft-tech", repo: "ci-github-publish" }, - ); + it("collects commit and pull request evidence through GitHub APIs", async () => { + const calls = []; + const service = new GitEvidenceService( + new ReferenceExtractor(), + createLoggerStub(), + { + rest: { + repos: { + async getCommit(parameters) { + calls.push(["getCommit", parameters]); + return { + data: { + sha: "1234567", + commit: { + message: "fix(ui): correct status badge", + }, + files: [ + { + filename: + "actions/release/summarize-changelog/src/index.js", + }, + { filename: "README.md" }, + ], + }, + }; + }, + }, + pulls: { + async get(parameters) { + calls.push(["getPull", parameters]); + return { + data: { + number: 42, + title: "feat(api): add project filters", + body: "Adds filters for technical users.", + }, + }; + }, + listFiles: {}, + }, + }, + async paginate(_method, parameters) { + calls.push(["paginate", parameters]); + return [ + { filename: "actions/release/summarize-changelog/action.yml" }, + { filename: "docs/guide.md" }, + ]; + }, + }, + { owner: "hoverkraft-tech", repo: "ci-github-publish" }, + ); - const evidence = await service.collect( - "fix(ui): correct status badge 1234567\nfeat(api): add project filters (#42)", - { - getRelativeWorkingDirectory() { - return "actions/release/summarize-changelog"; - }, - }, - ); + const evidence = await service.collect( + "fix(ui): correct status badge 1234567\nfeat(api): add project filters (#42)", + { + getRelativeWorkingDirectory() { + return "actions/release/summarize-changelog"; + }, + }, + ); - assert.match(evidence, /Commit 1234567:/); - assert.match(evidence, /Pull request #42:/); - assert.match(evidence, /fix\(ui\): correct status badge/); - assert.match(evidence, /feat\(api\): add project filters/); - assert.equal(calls.length, 3); - }); + assert.match(evidence, /Commit 1234567:/); + assert.match(evidence, /Pull request #42:/); + assert.match(evidence, /fix\(ui\): correct status badge/); + assert.match(evidence, /feat\(api\): add project filters/); + assert.equal(calls.length, 3); + }); - it("filters out evidence unrelated to the working directory", async () => { - const service = new GitEvidenceService( - new ReferenceExtractor(), - createLoggerStub(), - { - rest: { - repos: { - async getCommit() { - return { - data: { - sha: "1234567", - commit: { - message: "fix(ui): correct status badge", - }, - files: [{ filename: "docs/guide.md" }], - }, - }; - }, - }, - pulls: { - async get() { - return { - data: { - number: 42, - title: "feat(api): add project filters", - body: "Adds filters for technical users.", - }, - }; - }, - listFiles: {}, - }, - }, - async paginate() { - return [{ filename: "docs/guide.md" }]; - }, - }, - { owner: "hoverkraft-tech", repo: "ci-github-publish" }, - ); + it("filters out evidence unrelated to the working directory", async () => { + const service = new GitEvidenceService( + new ReferenceExtractor(), + createLoggerStub(), + { + rest: { + repos: { + async getCommit() { + return { + data: { + sha: "1234567", + commit: { + message: "fix(ui): correct status badge", + }, + files: [{ filename: "docs/guide.md" }], + }, + }; + }, + }, + pulls: { + async get() { + return { + data: { + number: 42, + title: "feat(api): add project filters", + body: "Adds filters for technical users.", + }, + }; + }, + listFiles: {}, + }, + }, + async paginate() { + return [{ filename: "docs/guide.md" }]; + }, + }, + { owner: "hoverkraft-tech", repo: "ci-github-publish" }, + ); - const evidence = await service.collect( - "fix(ui): correct status badge 1234567\nfeat(api): add project filters (#42)", - { - getRelativeWorkingDirectory() { - return "actions/release/summarize-changelog"; - }, - }, - ); + const evidence = await service.collect( + "fix(ui): correct status badge 1234567\nfeat(api): add project filters (#42)", + { + getRelativeWorkingDirectory() { + return "actions/release/summarize-changelog"; + }, + }, + ); - assert.equal(evidence, ""); - }); + assert.equal(evidence, ""); + }); }); diff --git a/actions/release/summarize-changelog/src/LinkEvidenceService.js b/actions/release/summarize-changelog/src/LinkEvidenceService.js index 5f6dcf7..669f5aa 100644 --- a/actions/release/summarize-changelog/src/LinkEvidenceService.js +++ b/actions/release/summarize-changelog/src/LinkEvidenceService.js @@ -4,91 +4,89 @@ const MAX_LINKS = 3; const MAX_EVIDENCE_LENGTH = 2000; const convertHtmlToText = compile({ - wordwrap: false, - selectors: [ - { selector: "script", format: "skip" }, - { selector: "style", format: "skip" }, - ], + wordwrap: false, + selectors: [ + { selector: "script", format: "skip" }, + { selector: "style", format: "skip" }, + ], }); export class LinkEvidenceService { - constructor(referenceExtractor, logger, fetchImpl = globalThis.fetch) { - this.referenceExtractor = referenceExtractor; - this.logger = logger; - this.fetch = fetchImpl; - } + constructor(referenceExtractor, logger, fetchImpl = globalThis.fetch) { + this.referenceExtractor = referenceExtractor; + this.logger = logger; + this.fetch = fetchImpl; + } - async collect(changelogBody) { - if (typeof this.fetch !== "function") { - return ""; - } + async collect(changelogBody) { + if (typeof this.fetch !== "function") { + return ""; + } - const references = this.referenceExtractor.extract(changelogBody); - const prioritizedUrls = this.#prioritizeUrls( - references.urls, - changelogBody, - ); - const evidenceBlocks = []; + const references = this.referenceExtractor.extract(changelogBody); + const prioritizedUrls = this.#prioritizeUrls( + references.urls, + changelogBody, + ); + const evidenceBlocks = []; - for (const url of prioritizedUrls.slice(0, MAX_LINKS)) { - try { - const response = await this.fetch(url, { - headers: { - "user-agent": "hoverkraft-release-summary-action", - }, - signal: AbortSignal.timeout(10000), - }); + for (const url of prioritizedUrls.slice(0, MAX_LINKS)) { + try { + const response = await this.fetch(url, { + headers: { + "user-agent": "hoverkraft-release-summary-action", + }, + signal: AbortSignal.timeout(10000), + }); - if (!response.ok) { - continue; - } + if (!response.ok) { + continue; + } - const responseText = await response.text(); - const normalized = this.#normalizeFetchedText(responseText); - if (normalized) { - evidenceBlocks.push( - `${url}\n${this.#limitText(normalized, MAX_EVIDENCE_LENGTH)}`, - ); - } - } catch { - continue; - } - } + const responseText = await response.text(); + const normalized = this.#normalizeFetchedText(responseText); + if (normalized) { + evidenceBlocks.push( + `${url}\n${this.#limitText(normalized, MAX_EVIDENCE_LENGTH)}`, + ); + } + } catch {} + } - if (evidenceBlocks.length > 0) { - this.logger.info( - `Collected ${evidenceBlocks.length} linked evidence block(s)`, - ); - } + if (evidenceBlocks.length > 0) { + this.logger.info( + `Collected ${evidenceBlocks.length} linked evidence block(s)`, + ); + } - return evidenceBlocks.join("\n\n").trim(); - } + return evidenceBlocks.join("\n\n").trim(); + } - #prioritizeUrls(urls, changelogBody) { - const breakingLines = changelogBody - .split(/\r?\n/) - .filter((line) => /breaking/i.test(line)); + #prioritizeUrls(urls, changelogBody) { + const breakingLines = changelogBody + .split(/\r?\n/) + .filter((line) => /breaking/i.test(line)); - return [...urls].sort((left, right) => { - const leftPriority = breakingLines.some((line) => line.includes(left)) - ? 0 - : 1; - const rightPriority = breakingLines.some((line) => line.includes(right)) - ? 0 - : 1; - return leftPriority - rightPriority; - }); - } + return [...urls].sort((left, right) => { + const leftPriority = breakingLines.some((line) => line.includes(left)) + ? 0 + : 1; + const rightPriority = breakingLines.some((line) => line.includes(right)) + ? 0 + : 1; + return leftPriority - rightPriority; + }); + } - #normalizeFetchedText(text) { - return convertHtmlToText(text).replace(/\s+/g, " ").trim(); - } + #normalizeFetchedText(text) { + return convertHtmlToText(text).replace(/\s+/g, " ").trim(); + } - #limitText(text, maxLength) { - if (text.length <= maxLength) { - return text; - } + #limitText(text, maxLength) { + if (text.length <= maxLength) { + return text; + } - return `${text.slice(0, maxLength - 1).trimEnd()}…`; - } + return `${text.slice(0, maxLength - 1).trimEnd()}…`; + } } diff --git a/actions/release/summarize-changelog/src/LinkEvidenceService.test.js b/actions/release/summarize-changelog/src/LinkEvidenceService.test.js index 24fd986..f56a778 100644 --- a/actions/release/summarize-changelog/src/LinkEvidenceService.test.js +++ b/actions/release/summarize-changelog/src/LinkEvidenceService.test.js @@ -3,39 +3,39 @@ import assert from "node:assert/strict"; import { LinkEvidenceService } from "./LinkEvidenceService.js"; describe("LinkEvidenceService", () => { - it("drops malformed script blocks when converting linked HTML to text", async () => { - const service = new LinkEvidenceService( - { - extract() { - return { - urls: ["https://example.com/release-notes"], - }; - }, - }, - { - info() {}, - }, - async () => ({ - ok: true, - async text() { - return [ - "", - "

Release Notes

", - '', - "

Visible summary text.

", - "", - ].join(""); - }, - }), - ); + it("drops malformed script blocks when converting linked HTML to text", async () => { + const service = new LinkEvidenceService( + { + extract() { + return { + urls: ["https://example.com/release-notes"], + }; + }, + }, + { + info() {}, + }, + async () => ({ + ok: true, + async text() { + return [ + "", + "

Release Notes

", + '', + "

Visible summary text.

", + "", + ].join(""); + }, + }), + ); - const evidence = await service.collect( - "BREAKING: see https://example.com/release-notes", - ); + const evidence = await service.collect( + "BREAKING: see https://example.com/release-notes", + ); - assert.match(evidence, /^https:\/\/example.com\/release-notes\n/); - assert.match(evidence, /visible summary text\./i); - assert.doesNotMatch(evidence, /alert\("xss"\)/); - assert.doesNotMatch(evidence, /script foo/); - }); + assert.match(evidence, /^https:\/\/example.com\/release-notes\n/); + assert.match(evidence, /visible summary text\./i); + assert.doesNotMatch(evidence, /alert\("xss"\)/); + assert.doesNotMatch(evidence, /script foo/); + }); }); diff --git a/actions/release/summarize-changelog/src/LlmSummaryService.js b/actions/release/summarize-changelog/src/LlmSummaryService.js index 0d3c024..997bd85 100644 --- a/actions/release/summarize-changelog/src/LlmSummaryService.js +++ b/actions/release/summarize-changelog/src/LlmSummaryService.js @@ -1,149 +1,149 @@ const SYSTEM_PROMPT = [ - "You write accurate release summaries for technical end users.", - "Use only facts present in the provided changelog and the confirmed evidence sections.", - "Never hallucinate, invent, infer, or guess.", - "Prioritize public-facing changes and mention internal changes only after a visible blank line.", - "When scopes are explicit, group the narrative by scope.", - "Order highlights as features, fixes, then internal changes.", - "The Release Summary section must stay within 5 sentences total, with no bullet points and no extra detail.", - "Return valid JSON only, with string properties `releaseSummary` and `breakingChanges`.", - "End with a dedicated Breaking changes section. If there is no confirmed breaking change, state that there is no breaking change.", - "If breaking changes reference URLs, use the linked evidence when it is available below.", + "You write accurate release summaries for technical end users.", + "Use only facts present in the provided changelog and the confirmed evidence sections.", + "Never hallucinate, invent, infer, or guess.", + "Prioritize public-facing changes and mention internal changes only after a visible blank line.", + "When scopes are explicit, group the narrative by scope.", + "Order highlights as features, fixes, then internal changes.", + "The Release Summary section must stay within 5 sentences total, with no bullet points and no extra detail.", + "Return valid JSON only, with string properties `releaseSummary` and `breakingChanges`.", + "End with a dedicated Breaking changes section. If there is no confirmed breaking change, state that there is no breaking change.", + "If breaking changes reference URLs, use the linked evidence when it is available below.", ].join(" "); const SUPPORTED_PROVIDERS = new Set(["openai", "anthropic", "google-genai"]); export class LlmSummaryService { - constructor(initChatModelImpl) { - this.initChatModelImpl = initChatModelImpl; - } - - async generate(inputs, llmPrompt) { - if (!SUPPORTED_PROVIDERS.has(inputs.llmProvider)) { - throw new Error( - "Unsupported llm-provider. Supported values: openai, anthropic, google-genai.", - ); - } - - const initChatModel = - this.initChatModelImpl || - (await import("langchain/chat_models/universal")).initChatModel; - - const resolvedModel = `${inputs.llmProvider}:${inputs.llmModel}`; - - const llmConfig = this.#normalizeLlmConfig({ - ...inputs.llmConfig, - apiKey: inputs.llmAuth, - }); - - const llm = await initChatModel(resolvedModel, llmConfig); - const response = await llm.invoke([ - { - role: "system", - content: SYSTEM_PROMPT, - }, - { - role: "user", - content: llmPrompt, - }, - ]); - - return this.#parseStructuredSummary( - this.#normalizeModelText(response?.content), - ); - } - - #normalizeLlmConfig(llmConfig) { - if (!llmConfig) { - return llmConfig; - } - - const normalizedConfig = { - ...llmConfig, - }; - - const baseUrl = normalizedConfig.baseUrl || normalizedConfig.baseURL; - - if (!baseUrl) { - return normalizedConfig; - } - - normalizedConfig.baseUrl = baseUrl; - - if (normalizedConfig.configuration?.baseURL) { - return normalizedConfig; - } - - return { - ...normalizedConfig, - configuration: { - ...(normalizedConfig.configuration || {}), - baseURL: baseUrl, - }, - }; - } - - #normalizeModelText(content) { - if (typeof content === "string") { - return content.trim(); - } - - if (Array.isArray(content)) { - return content - .map((part) => { - if (typeof part === "string") { - return part; - } - - if (typeof part?.text === "string") { - return part.text; - } - - return ""; - }) - .join("") - .trim(); - } - - return ""; - } - - #parseStructuredSummary(responseText) { - const sanitized = this.#stripCodeFence(responseText); - let parsed; - - try { - parsed = JSON.parse(sanitized); - } catch (error) { - throw new Error( - `The configured LLM returned invalid JSON: ${error.message}`, - ); - } - - const releaseSummary = parsed?.releaseSummary?.trim(); - const breakingChanges = parsed?.breakingChanges?.trim(); - - if (!releaseSummary) { - throw new Error( - "The configured LLM returned an invalid `releaseSummary` field.", - ); - } - - if (!breakingChanges) { - throw new Error( - "The configured LLM returned an invalid `breakingChanges` field.", - ); - } - - return { - releaseSummary, - breakingChanges, - }; - } - - #stripCodeFence(responseText) { - const trimmed = responseText.trim(); - const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); - return fencedMatch ? fencedMatch[1].trim() : trimmed; - } + constructor(initChatModelImpl) { + this.initChatModelImpl = initChatModelImpl; + } + + async generate(inputs, llmPrompt) { + if (!SUPPORTED_PROVIDERS.has(inputs.llmProvider)) { + throw new Error( + "Unsupported llm-provider. Supported values: openai, anthropic, google-genai.", + ); + } + + const initChatModel = + this.initChatModelImpl || + (await import("langchain/chat_models/universal")).initChatModel; + + const resolvedModel = `${inputs.llmProvider}:${inputs.llmModel}`; + + const llmConfig = this.#normalizeLlmConfig({ + ...inputs.llmConfig, + apiKey: inputs.llmAuth, + }); + + const llm = await initChatModel(resolvedModel, llmConfig); + const response = await llm.invoke([ + { + role: "system", + content: SYSTEM_PROMPT, + }, + { + role: "user", + content: llmPrompt, + }, + ]); + + return this.#parseStructuredSummary( + this.#normalizeModelText(response?.content), + ); + } + + #normalizeLlmConfig(llmConfig) { + if (!llmConfig) { + return llmConfig; + } + + const normalizedConfig = { + ...llmConfig, + }; + + const baseUrl = normalizedConfig.baseUrl || normalizedConfig.baseURL; + + if (!baseUrl) { + return normalizedConfig; + } + + normalizedConfig.baseUrl = baseUrl; + + if (normalizedConfig.configuration?.baseURL) { + return normalizedConfig; + } + + return { + ...normalizedConfig, + configuration: { + ...(normalizedConfig.configuration || {}), + baseURL: baseUrl, + }, + }; + } + + #normalizeModelText(content) { + if (typeof content === "string") { + return content.trim(); + } + + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === "string") { + return part; + } + + if (typeof part?.text === "string") { + return part.text; + } + + return ""; + }) + .join("") + .trim(); + } + + return ""; + } + + #parseStructuredSummary(responseText) { + const sanitized = this.#stripCodeFence(responseText); + let parsed; + + try { + parsed = JSON.parse(sanitized); + } catch (error) { + throw new Error( + `The configured LLM returned invalid JSON: ${error.message}`, + ); + } + + const releaseSummary = parsed?.releaseSummary?.trim(); + const breakingChanges = parsed?.breakingChanges?.trim(); + + if (!releaseSummary) { + throw new Error( + "The configured LLM returned an invalid `releaseSummary` field.", + ); + } + + if (!breakingChanges) { + throw new Error( + "The configured LLM returned an invalid `breakingChanges` field.", + ); + } + + return { + releaseSummary, + breakingChanges, + }; + } + + #stripCodeFence(responseText) { + const trimmed = responseText.trim(); + const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + return fencedMatch ? fencedMatch[1].trim() : trimmed; + } } diff --git a/actions/release/summarize-changelog/src/LlmSummaryService.test.js b/actions/release/summarize-changelog/src/LlmSummaryService.test.js index 3a0e185..60f4269 100644 --- a/actions/release/summarize-changelog/src/LlmSummaryService.test.js +++ b/actions/release/summarize-changelog/src/LlmSummaryService.test.js @@ -3,74 +3,74 @@ import assert from "node:assert/strict"; import { LlmSummaryService } from "./LlmSummaryService.js"; describe("LlmSummaryService", () => { - it("preserves initChatModel baseUrl config and populates OpenAI configuration.baseURL", async () => { - const initCalls = []; - const service = new LlmSummaryService(async (model, config) => { - initCalls.push({ model, config }); + it("preserves initChatModel baseUrl config and populates OpenAI configuration.baseURL", async () => { + const initCalls = []; + const service = new LlmSummaryService(async (model, config) => { + initCalls.push({ model, config }); - return { - async invoke() { - return { - content: JSON.stringify({ - releaseSummary: "Public API support was added.", - breakingChanges: "There is no breaking change.", - }), - }; - }, - }; - }); + return { + async invoke() { + return { + content: JSON.stringify({ + releaseSummary: "Public API support was added.", + breakingChanges: "There is no breaking change.", + }), + }; + }, + }; + }); - const summary = await service.generate( - { - llmProvider: "openai", - llmModel: "gpt-5.4", - llmAuth: "token", - llmConfig: { - baseUrl: "https://api.openai.com/v1", - temperature: 0.2, - }, - }, - "Summarize this changelog", - ); + const summary = await service.generate( + { + llmProvider: "openai", + llmModel: "gpt-5.4", + llmAuth: "token", + llmConfig: { + baseUrl: "https://api.openai.com/v1", + temperature: 0.2, + }, + }, + "Summarize this changelog", + ); - assert.deepEqual(initCalls, [ - { - model: "openai:gpt-5.4", - config: { - apiKey: "token", - baseUrl: "https://api.openai.com/v1", - configuration: { - baseURL: "https://api.openai.com/v1", - }, - temperature: 0.2, - }, - }, - ]); - assert.equal(summary.releaseSummary, "Public API support was added."); - assert.equal(summary.breakingChanges, "There is no breaking change."); - }); + assert.deepEqual(initCalls, [ + { + model: "openai:gpt-5.4", + config: { + apiKey: "token", + baseUrl: "https://api.openai.com/v1", + configuration: { + baseURL: "https://api.openai.com/v1", + }, + temperature: 0.2, + }, + }, + ]); + assert.equal(summary.releaseSummary, "Public API support was added."); + assert.equal(summary.breakingChanges, "There is no breaking change."); + }); - it("fails when the model response is not valid JSON", async () => { - const service = new LlmSummaryService(async () => ({ - async invoke() { - return { - content: "not-json", - }; - }, - })); + it("fails when the model response is not valid JSON", async () => { + const service = new LlmSummaryService(async () => ({ + async invoke() { + return { + content: "not-json", + }; + }, + })); - await assert.rejects( - () => - service.generate( - { - llmProvider: "anthropic", - llmModel: "claude-sonnet-4-6", - llmAuth: "token", - llmConfig: {}, - }, - "Summarize this changelog", - ), - /The configured LLM returned invalid JSON:/, - ); - }); + await assert.rejects( + () => + service.generate( + { + llmProvider: "anthropic", + llmModel: "claude-sonnet-4-6", + llmAuth: "token", + llmConfig: {}, + }, + "Summarize this changelog", + ), + /The configured LLM returned invalid JSON:/, + ); + }); }); diff --git a/actions/release/summarize-changelog/src/LoggerService.js b/actions/release/summarize-changelog/src/LoggerService.js index dfdff5b..d2e5a79 100644 --- a/actions/release/summarize-changelog/src/LoggerService.js +++ b/actions/release/summarize-changelog/src/LoggerService.js @@ -1,13 +1,13 @@ export class LoggerService { - constructor(core) { - this.core = core; - } + constructor(core) { + this.core = core; + } - info(message) { - this.core.info(message); - } + info(message) { + this.core.info(message); + } - warning(message) { - this.core.warning(message); - } + warning(message) { + this.core.warning(message); + } } diff --git a/actions/release/summarize-changelog/src/PromptBuilder.js b/actions/release/summarize-changelog/src/PromptBuilder.js index 8fe1cbf..a2fbd75 100644 --- a/actions/release/summarize-changelog/src/PromptBuilder.js +++ b/actions/release/summarize-changelog/src/PromptBuilder.js @@ -9,182 +9,182 @@ const MAX_LINK_EVIDENCE_LENGTH = 1500; const MAX_PROMPT_LENGTH = 12000; export const DEFAULT_SUMMARY_TEMPLATE = [ - "## Release Summary", - "", - RELEASE_SUMMARY_PLACEHOLDER, - "", - "## Breaking changes", - "", - BREAKING_CHANGES_PLACEHOLDER, + "## Release Summary", + "", + RELEASE_SUMMARY_PLACEHOLDER, + "", + "## Breaking changes", + "", + BREAKING_CHANGES_PLACEHOLDER, ].join("\n"); export class PromptBuilder { - build({ - summaryTemplate, - changelogBody, - workingDirectory, - gitEvidence, - linkEvidence, - }) { - const prunedTemplate = this.#pruneSection( - summaryTemplate.trim(), - MAX_TEMPLATE_LENGTH, - ); - let prunedChangelog = this.#pruneSection( - changelogBody, - MAX_CHANGELOG_LENGTH, - ); - let prunedGitEvidence = this.#pruneSection( - gitEvidence, - MAX_GIT_EVIDENCE_LENGTH, - ); - let prunedLinkEvidence = this.#pruneSection( - linkEvidence, - MAX_LINK_EVIDENCE_LENGTH, - ); - - const sections = [ - "Summarize the provided release changelog for technical end users.", - "", - "Required output template:", - prunedTemplate, - "", - "Template placeholders:", - `- ${RELEASE_SUMMARY_PLACEHOLDER}: replace with the release summary paragraph(s) only.`, - `- ${BREAKING_CHANGES_PLACEHOLDER}: replace with the breaking changes paragraph only.`, - "- Do not rewrite headings or surrounding template text.", - "", - "Rules:", - "- Highlight ordering: features, fixes, internal (chore, build, dev).", - "- Maximum 5 sentences in `## Release Summary`.", - "- No bullet points.", - "- No details.", - "- Separate public and internal changes with a blank line.", - "- Never mention uncertain items.", - "", - `Working directory focus: ${workingDirectory}`, - "", - "Changelog body:", - prunedChangelog, - ]; - - if (prunedGitEvidence) { - sections.push("", "Confirmed git evidence:", prunedGitEvidence); - } - - if (prunedLinkEvidence) { - sections.push("", "Linked reference evidence:", prunedLinkEvidence); - } - - sections.push( - "", - "Do not describe anything that is not explicit in the changelog body or confirmed evidence above.", - "Return only the two placeholder values as structured data, not the fully rendered markdown.", - ); - - let prompt = this.#normalizePrompt(sections.join("\n").trim()); - if (prompt.length <= MAX_PROMPT_LENGTH) { - return prompt; - } - - prunedLinkEvidence = this.#pruneSection(prunedLinkEvidence, 600); - prompt = this.#buildPromptWithPrunedSections({ - prunedTemplate, - prunedChangelog, - prunedGitEvidence, - prunedLinkEvidence, - workingDirectory, - }); - if (prompt.length <= MAX_PROMPT_LENGTH) { - return prompt; - } - - prunedGitEvidence = this.#pruneSection(prunedGitEvidence, 1200); - prompt = this.#buildPromptWithPrunedSections({ - prunedTemplate, - prunedChangelog, - prunedGitEvidence, - prunedLinkEvidence, - workingDirectory, - }); - if (prompt.length <= MAX_PROMPT_LENGTH) { - return prompt; - } - - prunedChangelog = this.#pruneSection(prunedChangelog, 3500); - return this.#buildPromptWithPrunedSections({ - prunedTemplate, - prunedChangelog, - prunedGitEvidence, - prunedLinkEvidence, - workingDirectory, - }); - } - - #buildPromptWithPrunedSections({ - prunedTemplate, - prunedChangelog, - prunedGitEvidence, - prunedLinkEvidence, - workingDirectory, - }) { - const sections = [ - "Summarize the provided release changelog for technical end users.", - "", - "Required output template:", - prunedTemplate, - "", - "Template placeholders:", - `- ${RELEASE_SUMMARY_PLACEHOLDER}: replace with the release summary paragraph(s) only.`, - `- ${BREAKING_CHANGES_PLACEHOLDER}: replace with the breaking changes paragraph only.`, - "- Do not rewrite headings or surrounding template text.", - "", - "Rules:", - "- Highlight ordering: features, fixes, internal (chore, build, dev).", - "- Maximum 5 sentences in `## Release Summary`.", - "- No bullet points.", - "- No details.", - "- Separate public and internal changes with a blank line.", - "- Never mention uncertain items.", - "", - `Working directory focus: ${workingDirectory}`, - "", - "Changelog body:", - prunedChangelog, - ]; - - if (prunedGitEvidence) { - sections.push("", "Confirmed git evidence:", prunedGitEvidence); - } - - if (prunedLinkEvidence) { - sections.push("", "Linked reference evidence:", prunedLinkEvidence); - } - - sections.push( - "", - "Do not describe anything that is not explicit in the changelog body or confirmed evidence above.", - "Return only the two placeholder values as structured data, not the fully rendered markdown.", - ); - - return this.#normalizePrompt(sections.join("\n").trim()); - } - - #pruneSection(content, maxLength) { - if (!content) { - return ""; - } - - const normalizedContent = this.#normalizePrompt(content.trim()); - if (normalizedContent.length <= maxLength) { - return normalizedContent; - } - - const sliceLength = Math.max(0, maxLength - TRUNCATION_NOTICE.length - 2); - return `${normalizedContent.slice(0, sliceLength).trimEnd()}\n${TRUNCATION_NOTICE}`; - } - - #normalizePrompt(content) { - return content.replace(/\n{3,}/g, "\n\n").trim(); - } + build({ + summaryTemplate, + changelogBody, + workingDirectory, + gitEvidence, + linkEvidence, + }) { + const prunedTemplate = this.#pruneSection( + summaryTemplate.trim(), + MAX_TEMPLATE_LENGTH, + ); + let prunedChangelog = this.#pruneSection( + changelogBody, + MAX_CHANGELOG_LENGTH, + ); + let prunedGitEvidence = this.#pruneSection( + gitEvidence, + MAX_GIT_EVIDENCE_LENGTH, + ); + let prunedLinkEvidence = this.#pruneSection( + linkEvidence, + MAX_LINK_EVIDENCE_LENGTH, + ); + + const sections = [ + "Summarize the provided release changelog for technical end users.", + "", + "Required output template:", + prunedTemplate, + "", + "Template placeholders:", + `- ${RELEASE_SUMMARY_PLACEHOLDER}: replace with the release summary paragraph(s) only.`, + `- ${BREAKING_CHANGES_PLACEHOLDER}: replace with the breaking changes paragraph only.`, + "- Do not rewrite headings or surrounding template text.", + "", + "Rules:", + "- Highlight ordering: features, fixes, internal (chore, build, dev).", + "- Maximum 5 sentences in `## Release Summary`.", + "- No bullet points.", + "- No details.", + "- Separate public and internal changes with a blank line.", + "- Never mention uncertain items.", + "", + `Working directory focus: ${workingDirectory}`, + "", + "Changelog body:", + prunedChangelog, + ]; + + if (prunedGitEvidence) { + sections.push("", "Confirmed git evidence:", prunedGitEvidence); + } + + if (prunedLinkEvidence) { + sections.push("", "Linked reference evidence:", prunedLinkEvidence); + } + + sections.push( + "", + "Do not describe anything that is not explicit in the changelog body or confirmed evidence above.", + "Return only the two placeholder values as structured data, not the fully rendered markdown.", + ); + + let prompt = this.#normalizePrompt(sections.join("\n").trim()); + if (prompt.length <= MAX_PROMPT_LENGTH) { + return prompt; + } + + prunedLinkEvidence = this.#pruneSection(prunedLinkEvidence, 600); + prompt = this.#buildPromptWithPrunedSections({ + prunedTemplate, + prunedChangelog, + prunedGitEvidence, + prunedLinkEvidence, + workingDirectory, + }); + if (prompt.length <= MAX_PROMPT_LENGTH) { + return prompt; + } + + prunedGitEvidence = this.#pruneSection(prunedGitEvidence, 1200); + prompt = this.#buildPromptWithPrunedSections({ + prunedTemplate, + prunedChangelog, + prunedGitEvidence, + prunedLinkEvidence, + workingDirectory, + }); + if (prompt.length <= MAX_PROMPT_LENGTH) { + return prompt; + } + + prunedChangelog = this.#pruneSection(prunedChangelog, 3500); + return this.#buildPromptWithPrunedSections({ + prunedTemplate, + prunedChangelog, + prunedGitEvidence, + prunedLinkEvidence, + workingDirectory, + }); + } + + #buildPromptWithPrunedSections({ + prunedTemplate, + prunedChangelog, + prunedGitEvidence, + prunedLinkEvidence, + workingDirectory, + }) { + const sections = [ + "Summarize the provided release changelog for technical end users.", + "", + "Required output template:", + prunedTemplate, + "", + "Template placeholders:", + `- ${RELEASE_SUMMARY_PLACEHOLDER}: replace with the release summary paragraph(s) only.`, + `- ${BREAKING_CHANGES_PLACEHOLDER}: replace with the breaking changes paragraph only.`, + "- Do not rewrite headings or surrounding template text.", + "", + "Rules:", + "- Highlight ordering: features, fixes, internal (chore, build, dev).", + "- Maximum 5 sentences in `## Release Summary`.", + "- No bullet points.", + "- No details.", + "- Separate public and internal changes with a blank line.", + "- Never mention uncertain items.", + "", + `Working directory focus: ${workingDirectory}`, + "", + "Changelog body:", + prunedChangelog, + ]; + + if (prunedGitEvidence) { + sections.push("", "Confirmed git evidence:", prunedGitEvidence); + } + + if (prunedLinkEvidence) { + sections.push("", "Linked reference evidence:", prunedLinkEvidence); + } + + sections.push( + "", + "Do not describe anything that is not explicit in the changelog body or confirmed evidence above.", + "Return only the two placeholder values as structured data, not the fully rendered markdown.", + ); + + return this.#normalizePrompt(sections.join("\n").trim()); + } + + #pruneSection(content, maxLength) { + if (!content) { + return ""; + } + + const normalizedContent = this.#normalizePrompt(content.trim()); + if (normalizedContent.length <= maxLength) { + return normalizedContent; + } + + const sliceLength = Math.max(0, maxLength - TRUNCATION_NOTICE.length - 2); + return `${normalizedContent.slice(0, sliceLength).trimEnd()}\n${TRUNCATION_NOTICE}`; + } + + #normalizePrompt(content) { + return content.replace(/\n{3,}/g, "\n\n").trim(); + } } diff --git a/actions/release/summarize-changelog/src/ReferenceExtractor.js b/actions/release/summarize-changelog/src/ReferenceExtractor.js index 3df7a72..1da0cfa 100644 --- a/actions/release/summarize-changelog/src/ReferenceExtractor.js +++ b/actions/release/summarize-changelog/src/ReferenceExtractor.js @@ -1,35 +1,35 @@ export class ReferenceExtractor { - extract(changelogBody) { - return { - commitShas: this.#uniqueMatches(changelogBody, /\b[0-9a-f]{7,40}\b/gi), - pullRequests: this.#uniqueMatches( - changelogBody, - /(?:^|[^\w])#(?\d+)\b|\/pull\/(?\d+)\b/gi, - ["number", "urlNumber"], - ), - urls: this.#uniqueMatches(changelogBody, /https?:\/\/[^\s)\]>]+/gi), - }; - } + extract(changelogBody) { + return { + commitShas: this.#uniqueMatches(changelogBody, /\b[0-9a-f]{7,40}\b/gi), + pullRequests: this.#uniqueMatches( + changelogBody, + /(?:^|[^\w])#(?\d+)\b|\/pull\/(?\d+)\b/gi, + ["number", "urlNumber"], + ), + urls: this.#uniqueMatches(changelogBody, /https?:\/\/[^\s)\]>]+/gi), + }; + } - #uniqueMatches(source, pattern, groupNames = []) { - const values = new Set(); - let match = pattern.exec(source); + #uniqueMatches(source, pattern, groupNames = []) { + const values = new Set(); + let match = pattern.exec(source); - while (match) { - if (groupNames.length > 0) { - for (const groupName of groupNames) { - const value = match.groups?.[groupName]; - if (value) { - values.add(value); - } - } - } else if (match[0]) { - values.add(match[0]); - } + while (match) { + if (groupNames.length > 0) { + for (const groupName of groupNames) { + const value = match.groups?.[groupName]; + if (value) { + values.add(value); + } + } + } else if (match[0]) { + values.add(match[0]); + } - match = pattern.exec(source); - } + match = pattern.exec(source); + } - return [...values]; - } + return [...values]; + } } diff --git a/actions/release/summarize-changelog/src/ReleaseSummaryCore.js b/actions/release/summarize-changelog/src/ReleaseSummaryCore.js index b4f5174..26ef221 100644 --- a/actions/release/summarize-changelog/src/ReleaseSummaryCore.js +++ b/actions/release/summarize-changelog/src/ReleaseSummaryCore.js @@ -1,152 +1,152 @@ import { - BREAKING_CHANGES_PLACEHOLDER, - DEFAULT_SUMMARY_TEMPLATE, - RELEASE_SUMMARY_PLACEHOLDER, + BREAKING_CHANGES_PLACEHOLDER, + DEFAULT_SUMMARY_TEMPLATE, + RELEASE_SUMMARY_PLACEHOLDER, } from "./PromptBuilder.js"; export class ReleaseSummaryCore { - constructor( - fileSystemService, - logger, - gitEvidenceService, - linkEvidenceService, - promptBuilder, - llmSummaryService, - ) { - this.fileSystemService = fileSystemService; - this.logger = logger; - this.gitEvidenceService = gitEvidenceService; - this.linkEvidenceService = linkEvidenceService; - this.promptBuilder = promptBuilder; - this.llmSummaryService = llmSummaryService; - } - - async summarize(inputs) { - const normalizedInputs = this.#normalizeInputs(inputs); - - this.logger.info( - `Working directory: ${this.fileSystemService.getRelativeWorkingDirectory() || "."}`, - ); - this.logger.info(`LLM provider: ${normalizedInputs.llmProvider}`); - this.logger.info(`LLM model: ${normalizedInputs.llmModel}`); - - const gitEvidence = await this.gitEvidenceService.collect( - normalizedInputs.changelogBody, - this.fileSystemService, - ); - const linkEvidence = await this.linkEvidenceService.collect( - normalizedInputs.changelogBody, - ); - - const llmPrompt = this.promptBuilder.build({ - summaryTemplate: normalizedInputs.summaryTemplate, - changelogBody: normalizedInputs.changelogBody, - workingDirectory: - this.fileSystemService.getRelativeWorkingDirectory() || ".", - gitEvidence, - linkEvidence, - }); - - const summarySections = await this.llmSummaryService.generate( - normalizedInputs, - llmPrompt, - ); - - const summary = this.#renderSummary( - normalizedInputs.summaryTemplate, - summarySections, - ); - - return { - llmPrompt, - summary, - }; - } - - #normalizeInputs(inputs) { - const changelogBody = (inputs.changelogBody || "").trim(); - const llmModel = (inputs.llmModel || "").trim(); - const llmProvider = (inputs.llmProvider || "openai").trim(); - const llmAuth = (inputs.llmAuth || "").trim(); - const llmConfig = this.#parseLlmConfig(inputs.llmConfig); - const summaryTemplate = inputs.summaryTemplate || DEFAULT_SUMMARY_TEMPLATE; - - if (!changelogBody) { - throw new Error("The changelog-body input is required."); - } - - if (!llmModel) { - throw new Error("The llm-model input is required."); - } - - if (!llmAuth) { - throw new Error("The llm-auth input is required."); - } - - if ( - this.#countOccurrences(summaryTemplate, RELEASE_SUMMARY_PLACEHOLDER) !== 1 - ) { - throw new Error( - "The summary-template input must include exactly one `{{release_summary}}` placeholder.", - ); - } - - if ( - this.#countOccurrences(summaryTemplate, BREAKING_CHANGES_PLACEHOLDER) !== - 1 - ) { - throw new Error( - "The summary-template input must include exactly one `{{breaking_changes}}` placeholder.", - ); - } - - return { - changelogBody, - llmModel, - llmProvider, - llmAuth, - llmConfig, - summaryTemplate, - }; - } - - #parseLlmConfig(rawValue) { - const llmConfigValue = (rawValue || "{}").trim() || "{}"; - let parsedValue; - - try { - parsedValue = JSON.parse(llmConfigValue); - } catch (error) { - throw new Error( - `The llm-config input must be valid JSON: ${error.message}`, - ); - } - - if ( - !parsedValue || - typeof parsedValue !== "object" || - Array.isArray(parsedValue) - ) { - throw new Error("The llm-config input must be a JSON object."); - } - - return parsedValue; - } - - #renderSummary(summaryTemplate, summarySections) { - const summary = summaryTemplate - .replace(RELEASE_SUMMARY_PLACEHOLDER, summarySections.releaseSummary) - .replace(BREAKING_CHANGES_PLACEHOLDER, summarySections.breakingChanges) - .trim(); - - if (!summary) { - throw new Error("The configured LLM returned an empty release summary."); - } - - return summary; - } - - #countOccurrences(source, value) { - return source.split(value).length - 1; - } + constructor( + fileSystemService, + logger, + gitEvidenceService, + linkEvidenceService, + promptBuilder, + llmSummaryService, + ) { + this.fileSystemService = fileSystemService; + this.logger = logger; + this.gitEvidenceService = gitEvidenceService; + this.linkEvidenceService = linkEvidenceService; + this.promptBuilder = promptBuilder; + this.llmSummaryService = llmSummaryService; + } + + async summarize(inputs) { + const normalizedInputs = this.#normalizeInputs(inputs); + + this.logger.info( + `Working directory: ${this.fileSystemService.getRelativeWorkingDirectory() || "."}`, + ); + this.logger.info(`LLM provider: ${normalizedInputs.llmProvider}`); + this.logger.info(`LLM model: ${normalizedInputs.llmModel}`); + + const gitEvidence = await this.gitEvidenceService.collect( + normalizedInputs.changelogBody, + this.fileSystemService, + ); + const linkEvidence = await this.linkEvidenceService.collect( + normalizedInputs.changelogBody, + ); + + const llmPrompt = this.promptBuilder.build({ + summaryTemplate: normalizedInputs.summaryTemplate, + changelogBody: normalizedInputs.changelogBody, + workingDirectory: + this.fileSystemService.getRelativeWorkingDirectory() || ".", + gitEvidence, + linkEvidence, + }); + + const summarySections = await this.llmSummaryService.generate( + normalizedInputs, + llmPrompt, + ); + + const summary = this.#renderSummary( + normalizedInputs.summaryTemplate, + summarySections, + ); + + return { + llmPrompt, + summary, + }; + } + + #normalizeInputs(inputs) { + const changelogBody = (inputs.changelogBody || "").trim(); + const llmModel = (inputs.llmModel || "").trim(); + const llmProvider = (inputs.llmProvider || "openai").trim(); + const llmAuth = (inputs.llmAuth || "").trim(); + const llmConfig = this.#parseLlmConfig(inputs.llmConfig); + const summaryTemplate = inputs.summaryTemplate || DEFAULT_SUMMARY_TEMPLATE; + + if (!changelogBody) { + throw new Error("The changelog-body input is required."); + } + + if (!llmModel) { + throw new Error("The llm-model input is required."); + } + + if (!llmAuth) { + throw new Error("The llm-auth input is required."); + } + + if ( + this.#countOccurrences(summaryTemplate, RELEASE_SUMMARY_PLACEHOLDER) !== 1 + ) { + throw new Error( + "The summary-template input must include exactly one `{{release_summary}}` placeholder.", + ); + } + + if ( + this.#countOccurrences(summaryTemplate, BREAKING_CHANGES_PLACEHOLDER) !== + 1 + ) { + throw new Error( + "The summary-template input must include exactly one `{{breaking_changes}}` placeholder.", + ); + } + + return { + changelogBody, + llmModel, + llmProvider, + llmAuth, + llmConfig, + summaryTemplate, + }; + } + + #parseLlmConfig(rawValue) { + const llmConfigValue = (rawValue || "{}").trim() || "{}"; + let parsedValue; + + try { + parsedValue = JSON.parse(llmConfigValue); + } catch (error) { + throw new Error( + `The llm-config input must be valid JSON: ${error.message}`, + ); + } + + if ( + !parsedValue || + typeof parsedValue !== "object" || + Array.isArray(parsedValue) + ) { + throw new Error("The llm-config input must be a JSON object."); + } + + return parsedValue; + } + + #renderSummary(summaryTemplate, summarySections) { + const summary = summaryTemplate + .replace(RELEASE_SUMMARY_PLACEHOLDER, summarySections.releaseSummary) + .replace(BREAKING_CHANGES_PLACEHOLDER, summarySections.breakingChanges) + .trim(); + + if (!summary) { + throw new Error("The configured LLM returned an empty release summary."); + } + + return summary; + } + + #countOccurrences(source, value) { + return source.split(value).length - 1; + } } diff --git a/actions/release/summarize-changelog/src/ReleaseSummaryCore.test.js b/actions/release/summarize-changelog/src/ReleaseSummaryCore.test.js index 89f764e..6f4e878 100644 --- a/actions/release/summarize-changelog/src/ReleaseSummaryCore.test.js +++ b/actions/release/summarize-changelog/src/ReleaseSummaryCore.test.js @@ -4,206 +4,206 @@ import { ReleaseSummaryCore } from "./ReleaseSummaryCore.js"; import { PromptBuilder } from "./PromptBuilder.js"; function createFileSystemServiceStub(relativeWorkingDirectory = ".") { - return { - getRelativeWorkingDirectory() { - return relativeWorkingDirectory; - }, - }; + return { + getRelativeWorkingDirectory() { + return relativeWorkingDirectory; + }, + }; } function createLoggerStub() { - return { - info() {}, - warning() {}, - }; + return { + info() {}, + warning() {}, + }; } describe("ReleaseSummaryCore", () => { - it("builds the prompt from evidence services and returns the generated summary", async () => { - const prompts = []; - const core = new ReleaseSummaryCore( - createFileSystemServiceStub("actions/release"), - createLoggerStub(), - { - async collect(changelogBody, fileSystemService) { - assert.match(changelogBody, /feat\(api\)/); - assert.equal( - fileSystemService.getRelativeWorkingDirectory(), - "actions/release", - ); - return "Commit 1234567:\nfix(ui): correct status badge"; - }, - }, - { - async collect(changelogBody) { - assert.match(changelogBody, /BREAKING:/); - return "https://example.com/breaking\nBreaking change details"; - }, - }, - new PromptBuilder(), - { - async generate(inputs, prompt) { - prompts.push(prompt); - assert.equal(inputs.llmProvider, "openai"); - assert.equal(inputs.llmModel, "gpt-5.4"); - return { - releaseSummary: [ - "Project filter support was added for public API consumers.", - "", - "Internal workflow maintenance was updated.", - ].join("\n"), - breakingChanges: "The deployment output name changed.", - }; - }, - }, - ); + it("builds the prompt from evidence services and returns the generated summary", async () => { + const prompts = []; + const core = new ReleaseSummaryCore( + createFileSystemServiceStub("actions/release"), + createLoggerStub(), + { + async collect(changelogBody, fileSystemService) { + assert.match(changelogBody, /feat\(api\)/); + assert.equal( + fileSystemService.getRelativeWorkingDirectory(), + "actions/release", + ); + return "Commit 1234567:\nfix(ui): correct status badge"; + }, + }, + { + async collect(changelogBody) { + assert.match(changelogBody, /BREAKING:/); + return "https://example.com/breaking\nBreaking change details"; + }, + }, + new PromptBuilder(), + { + async generate(inputs, prompt) { + prompts.push(prompt); + assert.equal(inputs.llmProvider, "openai"); + assert.equal(inputs.llmModel, "gpt-5.4"); + return { + releaseSummary: [ + "Project filter support was added for public API consumers.", + "", + "Internal workflow maintenance was updated.", + ].join("\n"), + breakingChanges: "The deployment output name changed.", + }; + }, + }, + ); - const result = await core.summarize({ - changelogBody: - "feat(api): add project filters (#42)\nBREAKING: renamed deployment output https://example.com/breaking", - llmModel: "gpt-5.4", - llmProvider: "openai", - llmAuth: "token", - llmConfig: JSON.stringify({ - baseUrl: "https://api.openai.com/v1", - }), - }); + const result = await core.summarize({ + changelogBody: + "feat(api): add project filters (#42)\nBREAKING: renamed deployment output https://example.com/breaking", + llmModel: "gpt-5.4", + llmProvider: "openai", + llmAuth: "token", + llmConfig: JSON.stringify({ + baseUrl: "https://api.openai.com/v1", + }), + }); - assert.match(result.summary, /^## Release Summary/m); - assert.match(result.summary, /^## Breaking changes/m); - assert.equal(prompts.length, 1); - assert.match(prompts[0], /Confirmed git evidence:/); - assert.match(prompts[0], /Linked reference evidence:/); - assert.match(prompts[0], /Maximum 5 sentences/); - assert.match(prompts[0], /\{\{release_summary\}\}/); - assert.match(prompts[0], /\{\{breaking_changes\}\}/); - }); + assert.match(result.summary, /^## Release Summary/m); + assert.match(result.summary, /^## Breaking changes/m); + assert.equal(prompts.length, 1); + assert.match(prompts[0], /Confirmed git evidence:/); + assert.match(prompts[0], /Linked reference evidence:/); + assert.match(prompts[0], /Maximum 5 sentences/); + assert.match(prompts[0], /\{\{release_summary\}\}/); + assert.match(prompts[0], /\{\{breaking_changes\}\}/); + }); - it("rejects templates that remove the breaking changes placeholder", async () => { - const core = new ReleaseSummaryCore( - createFileSystemServiceStub(), - createLoggerStub(), - { - async collect() { - return ""; - }, - }, - { - async collect() { - return ""; - }, - }, - new PromptBuilder(), - { - async generate() { - return ""; - }, - }, - ); + it("rejects templates that remove the breaking changes placeholder", async () => { + const core = new ReleaseSummaryCore( + createFileSystemServiceStub(), + createLoggerStub(), + { + async collect() { + return ""; + }, + }, + { + async collect() { + return ""; + }, + }, + new PromptBuilder(), + { + async generate() { + return ""; + }, + }, + ); - await assert.rejects( - () => - core.summarize({ - changelogBody: "feat: add search", - llmModel: "gpt-5.4", - llmProvider: "openai", - llmAuth: "token", - summaryTemplate: - "## Release Summary\n\n{{release_summary}}\n\n## Breaking changes", - }), - /The summary-template input must include exactly one `\{\{breaking_changes\}\}` placeholder\./, - ); - }); + await assert.rejects( + () => + core.summarize({ + changelogBody: "feat: add search", + llmModel: "gpt-5.4", + llmProvider: "openai", + llmAuth: "token", + summaryTemplate: + "## Release Summary\n\n{{release_summary}}\n\n## Breaking changes", + }), + /The summary-template input must include exactly one `\{\{breaking_changes\}\}` placeholder\./, + ); + }); - it("rejects llm-config values that are not JSON objects", async () => { - const core = new ReleaseSummaryCore( - createFileSystemServiceStub(), - createLoggerStub(), - { - async collect() { - return ""; - }, - }, - { - async collect() { - return ""; - }, - }, - new PromptBuilder(), - { - async generate() { - return ""; - }, - }, - ); + it("rejects llm-config values that are not JSON objects", async () => { + const core = new ReleaseSummaryCore( + createFileSystemServiceStub(), + createLoggerStub(), + { + async collect() { + return ""; + }, + }, + { + async collect() { + return ""; + }, + }, + new PromptBuilder(), + { + async generate() { + return ""; + }, + }, + ); - await assert.rejects( - () => - core.summarize({ - changelogBody: "feat: add search", - llmModel: "gpt-5.4", - llmProvider: "openai", - llmAuth: "token", - llmConfig: "[]", - }), - /The llm-config input must be a JSON object\./, - ); - }); + await assert.rejects( + () => + core.summarize({ + changelogBody: "feat: add search", + llmModel: "gpt-5.4", + llmProvider: "openai", + llmAuth: "token", + llmConfig: "[]", + }), + /The llm-config input must be a JSON object\./, + ); + }); - it("prunes oversized prompt sections to stay within a prompt budget", async () => { - const prompts = []; - const repeatedLine = - "feat(api): introduce a large amount of changelog context for token pruning checks (#42)"; - const largeChangelogBody = Array.from( - { length: 400 }, - () => repeatedLine, - ).join("\n"); - const largeGitEvidence = Array.from( - { length: 200 }, - (_, index) => - `Commit ${index}: details about changed files and implementation context`, - ).join("\n"); - const largeLinkEvidence = Array.from( - { length: 100 }, - (_, index) => - `https://example.com/${index} linked breaking-change context and release notes`, - ).join("\n"); + it("prunes oversized prompt sections to stay within a prompt budget", async () => { + const prompts = []; + const repeatedLine = + "feat(api): introduce a large amount of changelog context for token pruning checks (#42)"; + const largeChangelogBody = Array.from( + { length: 400 }, + () => repeatedLine, + ).join("\n"); + const largeGitEvidence = Array.from( + { length: 200 }, + (_, index) => + `Commit ${index}: details about changed files and implementation context`, + ).join("\n"); + const largeLinkEvidence = Array.from( + { length: 100 }, + (_, index) => + `https://example.com/${index} linked breaking-change context and release notes`, + ).join("\n"); - const core = new ReleaseSummaryCore( - createFileSystemServiceStub("actions/release"), - createLoggerStub(), - { - async collect() { - return largeGitEvidence; - }, - }, - { - async collect() { - return largeLinkEvidence; - }, - }, - new PromptBuilder(), - { - async generate(_inputs, prompt) { - prompts.push(prompt); - return { - releaseSummary: "Public API support was added.", - breakingChanges: "There is no breaking change.", - }; - }, - }, - ); + const core = new ReleaseSummaryCore( + createFileSystemServiceStub("actions/release"), + createLoggerStub(), + { + async collect() { + return largeGitEvidence; + }, + }, + { + async collect() { + return largeLinkEvidence; + }, + }, + new PromptBuilder(), + { + async generate(_inputs, prompt) { + prompts.push(prompt); + return { + releaseSummary: "Public API support was added.", + breakingChanges: "There is no breaking change.", + }; + }, + }, + ); - await core.summarize({ - changelogBody: largeChangelogBody, - llmModel: "gpt-5.4", - llmProvider: "openai", - llmAuth: "token", - llmConfig: "{}", - }); + await core.summarize({ + changelogBody: largeChangelogBody, + llmModel: "gpt-5.4", + llmProvider: "openai", + llmAuth: "token", + llmConfig: "{}", + }); - assert.equal(prompts.length, 1); - assert.ok(prompts[0].length <= 12000); - assert.match(prompts[0], /\[truncated for prompt budget\]/); - }); + assert.equal(prompts.length, 1); + assert.ok(prompts[0].length <= 12000); + assert.match(prompts[0], /\[truncated for prompt budget\]/); + }); }); diff --git a/actions/release/summarize-changelog/src/index.js b/actions/release/summarize-changelog/src/index.js index 2f994ec..a178e90 100644 --- a/actions/release/summarize-changelog/src/index.js +++ b/actions/release/summarize-changelog/src/index.js @@ -1,40 +1,40 @@ export async function run({ core, github, context, inputs, services }) { - try { - const { releaseSummaryCore } = - services || (await createDefaultServices(core, github, context, inputs)); + try { + const { releaseSummaryCore } = + services || (await createDefaultServices(core, github, context, inputs)); - const { llmPrompt, summary } = await releaseSummaryCore.summarize(inputs); + const { llmPrompt, summary } = await releaseSummaryCore.summarize(inputs); - core.setOutput("llm-prompt", llmPrompt); - core.setOutput("summary", summary); - } catch (error) { - core.setFailed(`Action failed: ${error.message}`); - throw error; - } + core.setOutput("llm-prompt", llmPrompt); + core.setOutput("summary", summary); + } catch (error) { + core.setFailed(`Action failed: ${error.message}`); + throw error; + } } async function createDefaultServices(core, github, context, inputs) { - const { FileSystemService } = await import("./FileSystemService.js"); - const { LoggerService } = await import("./LoggerService.js"); - const { ReferenceExtractor } = await import("./ReferenceExtractor.js"); - const { GitEvidenceService } = await import("./GitEvidenceService.js"); - const { LinkEvidenceService } = await import("./LinkEvidenceService.js"); - const { PromptBuilder } = await import("./PromptBuilder.js"); - const { LlmSummaryService } = await import("./LlmSummaryService.js"); - const { ReleaseSummaryCore } = await import("./ReleaseSummaryCore.js"); + const { FileSystemService } = await import("./FileSystemService.js"); + const { LoggerService } = await import("./LoggerService.js"); + const { ReferenceExtractor } = await import("./ReferenceExtractor.js"); + const { GitEvidenceService } = await import("./GitEvidenceService.js"); + const { LinkEvidenceService } = await import("./LinkEvidenceService.js"); + const { PromptBuilder } = await import("./PromptBuilder.js"); + const { LlmSummaryService } = await import("./LlmSummaryService.js"); + const { ReleaseSummaryCore } = await import("./ReleaseSummaryCore.js"); - const fileSystemService = new FileSystemService(inputs.workingDirectory); - const logger = new LoggerService(core); - const referenceExtractor = new ReferenceExtractor(); + const fileSystemService = new FileSystemService(inputs.workingDirectory); + const logger = new LoggerService(core); + const referenceExtractor = new ReferenceExtractor(); - return { - releaseSummaryCore: new ReleaseSummaryCore( - fileSystemService, - logger, - new GitEvidenceService(referenceExtractor, logger, github, context.repo), - new LinkEvidenceService(referenceExtractor, logger), - new PromptBuilder(), - new LlmSummaryService(), - ), - }; + return { + releaseSummaryCore: new ReleaseSummaryCore( + fileSystemService, + logger, + new GitEvidenceService(referenceExtractor, logger, github, context.repo), + new LinkEvidenceService(referenceExtractor, logger), + new PromptBuilder(), + new LlmSummaryService(), + ), + }; } diff --git a/actions/release/summarize-changelog/src/index.test.js b/actions/release/summarize-changelog/src/index.test.js index 8ea3dfd..7545dbc 100644 --- a/actions/release/summarize-changelog/src/index.test.js +++ b/actions/release/summarize-changelog/src/index.test.js @@ -3,55 +3,55 @@ import assert from "node:assert/strict"; import { run } from "./index.js"; describe("index.run", () => { - it("sets outputs from the release summary core flow", async () => { - const outputs = {}; - await run({ - core: { - info() {}, - warning() {}, - setOutput(name, value) { - outputs[name] = value; - }, - setFailed(message) { - throw new Error(message); - }, - }, - github: {}, - context: { - repo: { owner: "hoverkraft-tech", repo: "ci-github-publish" }, - }, - inputs: { - changelogBody: - "feat(api): add project filters (#42)\nBREAKING: renamed deployment output https://example.com/breaking", - workingDirectory: ".", - llmModel: "gpt-5.4", - llmProvider: "openai", - llmAuth: "token", - llmConfig: "{}", - }, - services: { - releaseSummaryCore: { - async summarize() { - return { - llmPrompt: - "Changelog body:\nfeat(api): add project filters (#42)", - summary: [ - "## Release Summary", - "", - "Project filter support was added for public API consumers.", - "", - "## Breaking changes", - "", - "There is no breaking change.", - ].join("\n"), - }; - }, - }, - }, - }); + it("sets outputs from the release summary core flow", async () => { + const outputs = {}; + await run({ + core: { + info() {}, + warning() {}, + setOutput(name, value) { + outputs[name] = value; + }, + setFailed(message) { + throw new Error(message); + }, + }, + github: {}, + context: { + repo: { owner: "hoverkraft-tech", repo: "ci-github-publish" }, + }, + inputs: { + changelogBody: + "feat(api): add project filters (#42)\nBREAKING: renamed deployment output https://example.com/breaking", + workingDirectory: ".", + llmModel: "gpt-5.4", + llmProvider: "openai", + llmAuth: "token", + llmConfig: "{}", + }, + services: { + releaseSummaryCore: { + async summarize() { + return { + llmPrompt: + "Changelog body:\nfeat(api): add project filters (#42)", + summary: [ + "## Release Summary", + "", + "Project filter support was added for public API consumers.", + "", + "## Breaking changes", + "", + "There is no breaking change.", + ].join("\n"), + }; + }, + }, + }, + }); - assert.match(outputs["llm-prompt"], /Changelog body:/); - assert.match(outputs["summary"], /^## Release Summary/m); - assert.match(outputs["summary"], /^## Breaking changes/m); - }); + assert.match(outputs["llm-prompt"], /Changelog body:/); + assert.match(outputs.summary, /^## Release Summary/m); + assert.match(outputs.summary, /^## Breaking changes/m); + }); }); diff --git a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/expected.yml b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/expected.yml index db3962c..da73a2b 100644 --- a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/expected.yml +++ b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/expected.yml @@ -1,11 +1,11 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: test-app-pr-100 # Will be updated by deploy worklfow + name: test-app-pr-100 # Will be updated by deploy workflow namespace: argocd annotations: - argocd.argoproj.io/deployment-id: "999" # Will be updated by deploy worklfow - argocd.argoproj.io/application-repository: test-app # Will be updated by deploy worklfow + argocd.argoproj.io/deployment-id: "999" # Will be updated by deploy workflow + argocd.argoproj.io/application-repository: test-app # Will be updated by deploy workflow argocd.argoproj.io/sync-wave: "4" labels: team: application @@ -16,7 +16,7 @@ metadata: spec: project: default destination: - namespace: test-app-pr-100 # Will be updated by deploy worklfow + namespace: test-app-pr-100 # Will be updated by deploy workflow server: https://test.com syncPolicy: syncOptions: @@ -38,9 +38,9 @@ spec: value: "0" helm: values: | - deploymentId: "999" # Will be updated by deploy worklfow + deploymentId: "999" # Will be updated by deploy workflow application: - appUri: # Will be updated by deploy worklfow + appUri: # Will be updated by deploy workflow dbMigrate: true dbSeed: true ingress: @@ -53,7 +53,7 @@ spec: alb.ingress.kubernetes.io/target-group-attributes: | deregistration_delay.timeout_seconds=30 hosts: - - host: https://pr-100-test-app.my-org.com # Will be updated by deploy worklfow + - host: https://pr-100-test-app.my-org.com # Will be updated by deploy workflow paths: - path: '' pathType: Prefix @@ -61,7 +61,7 @@ spec: admission.datadoghq.com/enabled: "true" tags.datadoghq.com/env: "review-app" tags.datadoghq.com/service: "test-app" - tags.datadoghq.com/version: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy worklfow + tags.datadoghq.com/version: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy workflow redis: enabled: true master: diff --git a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/template.yml.tpl b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/template.yml.tpl index 59cc867..8677f2a 100644 --- a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/template.yml.tpl +++ b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app-single-source/template.yml.tpl @@ -1,11 +1,11 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: # Will be updated by deploy worklfow + name: # Will be updated by deploy workflow namespace: argocd annotations: - argocd.argoproj.io/deployment-id: # Will be updated by deploy worklfow - argocd.argoproj.io/application-repository: # Will be updated by deploy worklfow + argocd.argoproj.io/deployment-id: # Will be updated by deploy workflow + argocd.argoproj.io/application-repository: # Will be updated by deploy workflow argocd.argoproj.io/sync-wave: "4" labels: team: application @@ -16,7 +16,7 @@ metadata: spec: project: default destination: - namespace: # Will be updated by deploy worklfow + namespace: # Will be updated by deploy workflow server: https://test.com syncPolicy: syncOptions: @@ -38,9 +38,9 @@ spec: value: "" helm: values: | - deploymentId: # Will be updated by deploy worklfow + deploymentId: # Will be updated by deploy workflow application: - appUri: # Will be updated by deploy worklfow + appUri: # Will be updated by deploy workflow dbMigrate: true dbSeed: true ingress: @@ -53,7 +53,7 @@ spec: alb.ingress.kubernetes.io/target-group-attributes: | deregistration_delay.timeout_seconds=30 hosts: - - host: # Will be updated by deploy worklfow + - host: # Will be updated by deploy workflow paths: - path: '' pathType: Prefix @@ -61,7 +61,7 @@ spec: admission.datadoghq.com/enabled: "true" tags.datadoghq.com/env: "review-app" tags.datadoghq.com/service: "test-app" - tags.datadoghq.com/version: # Will be updated by deploy worklfow + tags.datadoghq.com/version: # Will be updated by deploy workflow redis: enabled: true master: diff --git a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/expected.yml b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/expected.yml index 25b6843..b677983 100644 --- a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/expected.yml +++ b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/expected.yml @@ -1,11 +1,11 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: test-app-pr-100 # Will be updated by deploy worklfow + name: test-app-pr-100 # Will be updated by deploy workflow namespace: argocd annotations: - argocd.argoproj.io/deployment-id: "999" # Will be updated by deploy worklfow - argocd.argoproj.io/application-repository: test-app # Will be updated by deploy worklfow + argocd.argoproj.io/deployment-id: "999" # Will be updated by deploy workflow + argocd.argoproj.io/application-repository: test-app # Will be updated by deploy workflow argocd.argoproj.io/sync-wave: "4" labels: team: application @@ -16,7 +16,7 @@ metadata: spec: project: default destination: - namespace: test-app-pr-100 # Will be updated by deploy worklfow + namespace: test-app-pr-100 # Will be updated by deploy workflow server: https://test.com syncPolicy: syncOptions: @@ -28,12 +28,12 @@ spec: sources: - chart: test-app repoURL: ghcr.io/my-org/test-app/charts - targetRevision: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy worklfow + targetRevision: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy workflow helm: values: | - deploymentId: "999" # Will be updated by deploy worklfow + deploymentId: "999" # Will be updated by deploy workflow application: - appUri: # Will be updated by deploy worklfow + appUri: # Will be updated by deploy workflow dbMigrate: true dbSeed: true ingress: @@ -46,7 +46,7 @@ spec: alb.ingress.kubernetes.io/target-group-attributes: | deregistration_delay.timeout_seconds=30 hosts: - - host: https://pr-100-test-app.my-org.com # Will be updated by deploy worklfow + - host: https://pr-100-test-app.my-org.com # Will be updated by deploy workflow paths: - path: '' pathType: Prefix @@ -54,7 +54,7 @@ spec: admission.datadoghq.com/enabled: "true" tags.datadoghq.com/env: "review-app" tags.datadoghq.com/service: "test-app" - tags.datadoghq.com/version: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy worklfow + tags.datadoghq.com/version: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy workflow redis: enabled: true master: @@ -67,7 +67,7 @@ spec: enabled: false - chart: test-app repoURL: ghcr.io/my-org/test-app/charts - targetRevision: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy worklfow + targetRevision: 1.0.0-rc.0-pr-100-abac0800 # Will be updated by deploy workflow plugin: name: hoverkraft-deployment env: diff --git a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/template.yml.tpl b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/template.yml.tpl index c76476e..7a45226 100644 --- a/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/template.yml.tpl +++ b/tests/argocd-app-of-apps/ci/apps/ci-test/test-app/template.yml.tpl @@ -1,11 +1,11 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: # Will be updated by deploy worklfow + name: # Will be updated by deploy workflow namespace: argocd annotations: - argocd.argoproj.io/deployment-id: # Will be updated by deploy worklfow - argocd.argoproj.io/application-repository: # Will be updated by deploy worklfow + argocd.argoproj.io/deployment-id: # Will be updated by deploy workflow + argocd.argoproj.io/application-repository: # Will be updated by deploy workflow argocd.argoproj.io/sync-wave: "4" labels: team: application @@ -16,7 +16,7 @@ metadata: spec: project: default destination: - namespace: # Will be updated by deploy worklfow + namespace: # Will be updated by deploy workflow server: https://test.com syncPolicy: syncOptions: @@ -28,12 +28,12 @@ spec: sources: - chart: test-app repoURL: ghcr.io/my-org/test-app/charts - targetRevision: # Will be updated by deploy worklfow + targetRevision: # Will be updated by deploy workflow helm: values: | - deploymentId: # Will be updated by deploy worklfow + deploymentId: # Will be updated by deploy workflow application: - appUri: # Will be updated by deploy worklfow + appUri: # Will be updated by deploy workflow dbMigrate: true dbSeed: true ingress: @@ -46,7 +46,7 @@ spec: alb.ingress.kubernetes.io/target-group-attributes: | deregistration_delay.timeout_seconds=30 hosts: - - host: # Will be updated by deploy worklfow + - host: # Will be updated by deploy workflow paths: - path: '' pathType: Prefix @@ -54,7 +54,7 @@ spec: admission.datadoghq.com/enabled: "true" tags.datadoghq.com/env: "review-app" tags.datadoghq.com/service: "test-app" - tags.datadoghq.com/version: # Will be updated by deploy worklfow + tags.datadoghq.com/version: # Will be updated by deploy workflow redis: enabled: true master: @@ -67,7 +67,7 @@ spec: enabled: false - chart: test-app repoURL: ghcr.io/my-org/test-app/charts - targetRevision: # Will be updated by deploy worklfow + targetRevision: # Will be updated by deploy workflow plugin: name: hoverkraft-deployment env: diff --git a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/expected.yml b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/expected.yml index 90d9ba6..6ea63c7 100644 --- a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/expected.yml +++ b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/expected.yml @@ -1,8 +1,8 @@ apiVersion: v1 kind: Namespace metadata: - name: test-app-pr-100 # Will be updated by deploy worklfow + name: test-app-pr-100 # Will be updated by deploy workflow annotations: - app.kubernetes.io/instance: test-app-pr-100 # Will be updated by deploy worklfow + app.kubernetes.io/instance: test-app-pr-100 # Will be updated by deploy workflow argocd.argoproj.io/sync-options: Prune=true argocd.argoproj.io/sync-wave: "0" diff --git a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/template.yml.tpl b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/template.yml.tpl index 8a243cf..a0016cc 100644 --- a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/template.yml.tpl +++ b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app-single-source/template.yml.tpl @@ -1,8 +1,8 @@ apiVersion: v1 kind: Namespace metadata: - name: # Will be updated by deploy worklfow + name: # Will be updated by deploy workflow annotations: - app.kubernetes.io/instance: # Will be updated by deploy worklfow + app.kubernetes.io/instance: # Will be updated by deploy workflow argocd.argoproj.io/sync-options: Prune=true argocd.argoproj.io/sync-wave: "0" diff --git a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/expected.yml b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/expected.yml index 90d9ba6..6ea63c7 100644 --- a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/expected.yml +++ b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/expected.yml @@ -1,8 +1,8 @@ apiVersion: v1 kind: Namespace metadata: - name: test-app-pr-100 # Will be updated by deploy worklfow + name: test-app-pr-100 # Will be updated by deploy workflow annotations: - app.kubernetes.io/instance: test-app-pr-100 # Will be updated by deploy worklfow + app.kubernetes.io/instance: test-app-pr-100 # Will be updated by deploy workflow argocd.argoproj.io/sync-options: Prune=true argocd.argoproj.io/sync-wave: "0" diff --git a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/template.yml.tpl b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/template.yml.tpl index 8a243cf..a0016cc 100644 --- a/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/template.yml.tpl +++ b/tests/argocd-app-of-apps/ci/manifests/ci-test/test-app/template.yml.tpl @@ -1,8 +1,8 @@ apiVersion: v1 kind: Namespace metadata: - name: # Will be updated by deploy worklfow + name: # Will be updated by deploy workflow annotations: - app.kubernetes.io/instance: # Will be updated by deploy worklfow + app.kubernetes.io/instance: # Will be updated by deploy workflow argocd.argoproj.io/sync-options: Prune=true argocd.argoproj.io/sync-wave: "0"