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 = [
- "",
- '
',
- '',
- "",
- ].join("\n");
+ const assetManager = new AssetManager({ workspacePath, sitePath });
+ const content = [
+ "",
+ '
',
+ '',
+ "",
+ ].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 %}",
- "",
- "",
- ].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 %}",
+ "",
+ "",
+ ].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).",
- "",
- "",
- ].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).",
+ "",
+ "",
+ ].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"