Compare commits

..

1 Commits

Author SHA1 Message Date
GitHub Action 6f7c489cec build 2025-08-06 17:20:35 +00:00
85 changed files with 28807 additions and 2945 deletions
-53
View File
@@ -1,53 +0,0 @@
name: 'Setup Minikube Test Environment'
description: 'Common setup steps for minikube integration tests'
inputs:
install-smi:
description: 'Install Linkerd SMI for service mesh tests'
required: false
default: 'false'
runs:
using: 'composite'
steps:
- name: Install dependencies
shell: bash
run: |
rm -rf node_modules/
npm install
- name: Build
shell: bash
run: npm run build
- name: Install conntrack
shell: bash
run: sudo apt-get install -y conntrack
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e9e035a86bbc3caea26a450bd4dbf9d0c453682e # v0.0.21
with:
minikube-version: 1.37.0
kubernetes-version: 1.31.0
driver: 'docker'
- name: Install Linkerd and SMI
if: inputs.install-smi == 'true'
shell: bash
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # 6.1.0
name: Install Python
with:
python-version: '3.x'
+4 -4
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -24,7 +24,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e #v3.29.5
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d #v3.29.5
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@@ -32,7 +32,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e #v3.29.5
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d #v3.29.5
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -46,4 +46,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e #v3.29.5
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d #v3.29.5
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
name: Setting issue as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -24,7 +24,7 @@ jobs:
operations-per-run: 100
exempt-issue-labels: 'backlog'
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
name: Setting PR as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: install deps
run: npm install
+1 -1
View File
@@ -13,6 +13,6 @@ jobs:
permissions:
actions: read
contents: write
uses: Azure/action-release-workflows/.github/workflows/release_js_project.yaml@3c677ba5ab58f5c5c1a6f0cfb176b333b1f27405 # v1
uses: Azure/action-release-workflows/.github/workflows/release_js_project.yaml@v1
with:
changelogPath: ./CHANGELOG.md
@@ -18,15 +18,39 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
@@ -12,21 +12,45 @@ on:
jobs:
run-integration-test:
name: Blue-Green Ingress Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'nginx-service' ${{ env.NAMESPACE }}
@@ -12,21 +12,45 @@ on:
jobs:
run-integration-test:
name: Blue-Green Service Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
@@ -12,23 +12,56 @@ on:
jobs:
run-integration-test:
name: Blue-Green SMI Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
install-smi: 'true'
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Install linkerd and add controlplane to cluster
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
@@ -12,21 +12,45 @@ on:
jobs:
run-integration-test:
name: Canary Pod Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
@@ -12,23 +12,56 @@ on:
jobs:
run-integration-test:
name: Canary SMI Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
install-smi: 'true'
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Install Linkerd and SMI
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
@@ -14,18 +14,37 @@ on:
jobs:
run-integration-test:
name: Namespace Optional Tests
name: Run Namespace Optional Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE1: integration-test-namespace1-${{ github.run_id }}
NAMESPACE2: integration-test-namespace2-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespaces for tests
run: |
@@ -33,6 +52,11 @@ jobs:
kubectl create ns ${{ env.NAMESPACE2 }}
kubectl create ns test-namespace
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Deployment' 'all' ${{ env.NAMESPACE1 }}
@@ -19,24 +19,24 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Build
run: npm run build
run: ncc build src/run.ts -o lib
- name: Azure login
uses: azure/login@v3.0.0
uses: azure/login@v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: Azure/setup-kubectl@829323503d1be3d00ca8346e5391ca0b07a9ab0d # v5.1.0
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- name: Create private AKS cluster and set context
@@ -51,7 +51,7 @@ jobs:
run: |
az aks command invoke --resource-group ${{ env.NAMESPACE }} --name ${{ env.NAMESPACE }} --command "kubectl create ns ${{ env.NAMESPACE }}"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 6.2.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
@@ -12,21 +12,45 @@ on:
jobs:
run-integration-test:
name: Resource Annotation Tests
name: Run Minikube Integration Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e3c7f79eb1e997eabccc536a6cf318a2b0fe19d9 # v0.0.20
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: |
npm install
npm test
-1
View File
@@ -2,6 +2,5 @@ node_modules
.DS_Store
.idea
lib/
coverage/
-6
View File
@@ -1,9 +1,3 @@
npm run typecheck || {
echo ""
echo "❌ Type check failed."
echo "💡 Run 'npm run typecheck' to see errors."
exit 1
}
npm test
npm run format-check || {
echo ""
-26
View File
@@ -1,31 +1,5 @@
# Changelog
## [6.0.0] - 2026-04-17
### Changed
- #504 [Update Node.js runtime from node20 to node24](https://github.com/Azure/k8s-deploy/pull/504)
- #500 [Update action version references in README to latest majors](https://github.com/Azure/k8s-deploy/pull/500)
### Security
- #506 [Bump undici from 6.23.0 to 6.24.1](https://github.com/Azure/k8s-deploy/pull/506)
- #513 [Bump vite from 8.0.3 to 8.0.5](https://github.com/Azure/k8s-deploy/pull/513)
- #509 [Bump the actions group across 1 directory with 4 updates](https://github.com/Azure/k8s-deploy/pull/509)
- #510 [Bump the actions group across 1 directory with 2 updates](https://github.com/Azure/k8s-deploy/pull/510)
- #511 [Bump vitest from 4.1.1 to 4.1.2](https://github.com/Azure/k8s-deploy/pull/511)
- #514 [Bump the actions group with 2 updates](https://github.com/Azure/k8s-deploy/pull/514)
- #501 [Bump @types/node from 25.3.3 to 25.4.0](https://github.com/Azure/k8s-deploy/pull/501)
## [5.1.0] - 2026-03-03
### Added
- #458 [Ensure error messages display the correct namespace](https://github.com/Azure/k8s-deploy/pull/458)
- #482 [docker driver](https://github.com/Azure/k8s-deploy/pull/482)
- #492 [Migrate to esbuild/Vitest and upgrade @actions/\* to ESM-only versions](https://github.com/Azure/k8s-deploy/pull/492)
- #498 [Add typecheck to build script](https://github.com/Azure/k8s-deploy/pull/498)
## [5.0.4] - 2025-08-05
### Added
+13 -13
View File
@@ -1,6 +1,6 @@
# Deploy manifests action for Kubernetes
This action is used to deploy manifests to Kubernetes clusters. It requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v4) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v4) action. It also requires Kubectl to be installed (you can use the [Azure/setup-kubectl](https://github.com/Azure/setup-kubectl) action).
This action is used to deploy manifests to Kubernetes clusters. It requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v1) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v1) action. It also requires Kubectl to be installed (you can use the [Azure/setup-kubectl](https://github.com/Azure/setup-kubectl) action).
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
@@ -310,9 +310,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: Azure/docker-login@v2
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -331,7 +331,7 @@ jobs:
cluster-name: contoso
resource-group: contoso-rg
- uses: Azure/k8s-create-secret@v5
- uses: Azure/k8s-create-secret@v4
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
@@ -359,9 +359,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: Azure/docker-login@v2
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -377,14 +377,14 @@ jobs:
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- uses: Azure/k8s-create-secret@v5
- uses: Azure/k8s-create-secret@v4
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v5
- uses: Azure/k8s-deploy@v4
with:
action: deploy
manifests: |
@@ -409,9 +409,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: Azure/docker-login@v2
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -433,9 +433,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@master
- uses: Azure/docker-login@v2
- uses: Azure/docker-login@v1
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -450,7 +450,7 @@ jobs:
cluster-name: contoso
resource-group: contoso-rg
- uses: Azure/k8s-create-secret@v5
- uses: Azure/k8s-create-secret@v4
with:
namespace: ${{ env.NAMESPACE }}
container-registry-url: contoso.azurecr.io
+1 -1
View File
@@ -96,5 +96,5 @@ inputs:
branding:
color: 'green'
runs:
using: 'node24'
using: 'node20'
main: 'lib/index.js'
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'\\.[jt]sx?$': 'babel-jest'
},
transformIgnorePatterns: [
'node_modules/(?!' +
[
'@octokit',
'universal-user-agent',
'before-after-hook',
'minimist'
].join('|') +
')'
],
verbose: true,
testTimeout: 9000
}
+18240
View File
File diff suppressed because it is too large Load Diff
+9609 -1583
View File
File diff suppressed because it is too large Load Diff
+23 -19
View File
@@ -1,36 +1,40 @@
{
"name": "k8s-deploy-action",
"version": "6.0.0",
"version": "5.0.0",
"author": "Deepak Sattiraju",
"license": "MIT",
"type": "module",
"scripts": {
"build": "tsc --noEmit && esbuild src/run.ts --bundle --platform=node --target=node20 --format=esm --outfile=lib/index.js --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"coverage": "vitest run --coverage",
"prebuild": "npm i @vercel/ncc",
"build": "ncc build src/run.ts -o lib",
"test": "jest",
"coverage": "jest --coverage=true",
"format": "prettier --write .",
"format-check": "prettier --check .",
"prepare": "husky"
},
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "4.0.0",
"@octokit/core": "^7.0.6",
"@octokit/plugin-retry": "^8.1.0",
"js-yaml": "4.2.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.0.0",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "2.0.2",
"@babel/preset-env": "^7.28.0",
"@babel/preset-typescript": "^7.27.1",
"@octokit/core": "^7.0.3",
"@octokit/plugin-retry": "^8.0.1",
"@types/minipass": "^3.3.5",
"husky": "^9.1.7",
"js-yaml": "4.1.0",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^25.9.3",
"esbuild": "^0.28",
"husky": "^9.1.7",
"prettier": "^3.8.4",
"typescript": "6.0.3",
"vitest": "^4"
"@types/node": "^24.2.0",
"@vercel/ncc": "^0.38.3",
"jest": "^30.0.5",
"prettier": "^3.6.2",
"ts-jest": "^29.4.1",
"typescript": "5.9.2"
}
}
+8 -8
View File
@@ -1,19 +1,19 @@
import * as core from '@actions/core'
import * as models from '../types/kubernetesTypes.js'
import * as KubernetesConstants from '../types/kubernetesTypes.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import * as models from '../types/kubernetesTypes'
import * as KubernetesConstants from '../types/kubernetesTypes'
import {Kubectl, Resource} from '../types/kubectl'
import {
getResources,
updateManifestFiles
} from '../utilities/manifestUpdateUtils.js'
} from '../utilities/manifestUpdateUtils'
import {
annotateAndLabelResources,
checkManifestStability,
deployManifests
} from '../strategyHelpers/deploymentHelper.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod.js'
import {ClusterType} from '../inputUtils.js'
} from '../strategyHelpers/deploymentHelper'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod'
import {ClusterType} from '../inputUtils'
export const ResourceTypeManagedCluster =
'Microsoft.ContainerService/managedClusters'
export const ResourceTypeFleet = 'Microsoft.ContainerService/fleets'
+18 -18
View File
@@ -1,44 +1,44 @@
import * as core from '@actions/core'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper.js'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper.js'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper.js'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper'
import {
getResources,
updateManifestFiles
} from '../utilities/manifestUpdateUtils.js'
import {annotateAndLabelResources} from '../strategyHelpers/deploymentHelper.js'
import * as models from '../types/kubernetesTypes.js'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils.js'
} from '../utilities/manifestUpdateUtils'
import {annotateAndLabelResources} from '../strategyHelpers/deploymentHelper'
import * as models from '../types/kubernetesTypes'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
import {
deleteGreenObjects,
getManifestObjects,
NONE_LABEL_VALUE
} from '../strategyHelpers/blueGreen/blueGreenHelper.js'
} from '../strategyHelpers/blueGreen/blueGreenHelper'
import {BlueGreenManifests} from '../types/blueGreenTypes.js'
import {DeployResult} from '../types/deployResult.js'
import {BlueGreenManifests} from '../types/blueGreenTypes'
import {DeployResult} from '../types/deployResult'
import {
promoteBlueGreenIngress,
promoteBlueGreenService,
promoteBlueGreenSMI
} from '../strategyHelpers/blueGreen/promote.js'
} from '../strategyHelpers/blueGreen/promote'
import {
routeBlueGreenService,
routeBlueGreenIngressUnchanged,
routeBlueGreenSMI
} from '../strategyHelpers/blueGreen/route.js'
} from '../strategyHelpers/blueGreen/route'
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
import {Kubectl, Resource} from '../types/kubectl'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy.js'
import {ClusterType} from '../inputUtils.js'
} from '../types/trafficSplitMethod'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
import {ClusterType} from '../inputUtils'
export async function promote(
kubectl: Kubectl,
+9 -9
View File
@@ -1,20 +1,20 @@
import * as core from '@actions/core'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper.js'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper.js'
import {Kubectl} from '../types/kubectl.js'
import {BlueGreenManifests} from '../types/blueGreenTypes.js'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import {Kubectl} from '../types/kubectl'
import {BlueGreenManifests} from '../types/blueGreenTypes'
import {
rejectBlueGreenIngress,
rejectBlueGreenService,
rejectBlueGreenSMI
} from '../strategyHelpers/blueGreen/reject.js'
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
} from '../strategyHelpers/blueGreen/reject'
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy.js'
} from '../types/trafficSplitMethod'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function reject(
kubectl: Kubectl,
+2 -5
View File
@@ -1,8 +1,5 @@
import {parseResourceTypeInput} from './inputUtils.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
import {parseResourceTypeInput} from './inputUtils'
import {ResourceTypeFleet, ResourceTypeManagedCluster} from './actions/deploy'
describe('InputUtils', () => {
describe('parseResourceTypeInput', () => {
+2 -5
View File
@@ -1,9 +1,6 @@
import * as core from '@actions/core'
import {parseAnnotations} from './types/annotations.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
import {parseAnnotations} from './types/annotations'
import {ResourceTypeFleet, ResourceTypeManagedCluster} from './actions/deploy'
export const inputAnnotations = parseAnnotations(
core.getInput('annotations', {required: false})
+11 -11
View File
@@ -1,19 +1,19 @@
import * as core from '@actions/core'
import {getKubectlPath, Kubectl} from './types/kubectl.js'
import {getKubectlPath, Kubectl} from './types/kubectl'
import {
deploy,
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
import {ClusterType} from './inputUtils.js'
import {promote} from './actions/promote.js'
import {reject} from './actions/reject.js'
import {Action, parseAction} from './types/action.js'
import {parseDeploymentStrategy} from './types/deploymentStrategy.js'
import {getFilesFromDirectoriesAndURLs} from './utilities/fileUtils.js'
import {PrivateKubectl} from './types/privatekubectl.js'
import {parseResourceTypeInput} from './inputUtils.js'
import {parseDuration} from './utilities/durationUtils.js'
} from './actions/deploy'
import {ClusterType} from './inputUtils'
import {promote} from './actions/promote'
import {reject} from './actions/reject'
import {Action, parseAction} from './types/action'
import {parseDeploymentStrategy} from './types/deploymentStrategy'
import {getFilesFromDirectoriesAndURLs} from './utilities/fileUtils'
import {PrivateKubectl} from './types/privatekubectl'
import {parseResourceTypeInput} from './inputUtils'
import {parseDuration} from './utilities/durationUtils'
export async function run() {
// verify kubeconfig is set
@@ -1,5 +1,3 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {
deployWithLabel,
deleteGreenObjects,
@@ -10,16 +8,16 @@ import {
getNewBlueGreenObject,
GREEN_LABEL_VALUE,
isServiceRouted
} from './blueGreenHelper.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import * as bgHelper from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import {K8sObject} from '../../types/k8sObject.js'
import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils.js'
} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import * as bgHelper from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import {K8sObject} from '../../types/k8sObject'
import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils'
import {ExecOutput} from '@actions/exec'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const kubectl = new Kubectl('')
const TEST_TIMEOUT = '60s'
@@ -39,17 +37,17 @@ const MOCK_EXEC_OUTPUT = {
describe('bluegreenhelper functions', () => {
let testObjects
beforeEach(() => {
vi.restoreAllMocks()
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(['test/unit/manifests/test-ingress.yml'])
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('correctly deletes services and workloads according to label', async () => {
vi.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
const value = await deleteGreenObjects(
kubectl,
@@ -67,16 +65,21 @@ describe('bluegreenhelper functions', () => {
})
test('handles timeout when deleting objects', async () => {
const deleteMock = vi.fn().mockResolvedValue(MOCK_EXEC_OUTPUT)
kubectl.delete = deleteMock
// Mock deleteObjects to prevent actual execution
const deleteSpy = jest
.spyOn(kubectl, 'delete')
.mockResolvedValue(MOCK_EXEC_OUTPUT)
const deleteList = EXPECTED_GREEN_OBJECTS
await bgHelper.deleteObjects(
kubectl,
EXPECTED_GREEN_OBJECTS,
TEST_TIMEOUT
)
await bgHelper.deleteObjects(kubectl, deleteList, TEST_TIMEOUT)
expect(deleteMock).toHaveBeenCalledTimes(deleteList.length)
deleteList.forEach(({name, kind}) => {
expect(deleteMock).toHaveBeenCalledWith(
// Verify kubectl.delete is called with timeout for each object in deleteList
expect(deleteSpy).toHaveBeenCalledTimes(EXPECTED_GREEN_OBJECTS.length)
EXPECTED_GREEN_OBJECTS.forEach(({name, kind}) => {
expect(deleteSpy).toHaveBeenCalledWith(
[kind, name],
undefined,
TEST_TIMEOUT
@@ -132,7 +135,7 @@ describe('bluegreenhelper functions', () => {
})
test('correctly makes labeled workloads', async () => {
const kubectlApplySpy = vi.spyOn(kubectl, 'apply').mockResolvedValue({
const kubectlApplySpy = jest.spyOn(kubectl, 'apply').mockResolvedValue({
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
@@ -176,9 +179,9 @@ describe('bluegreenhelper functions', () => {
exitCode: 0
}
vi.spyOn(kubectl, 'getResource').mockImplementation(() =>
Promise.resolve(mockExecOutput)
)
jest
.spyOn(kubectl, 'getResource')
.mockImplementation(() => Promise.resolve(mockExecOutput))
const fetched = await fetchResource(
kubectl,
'nginx-deployment',
@@ -206,12 +209,12 @@ describe('bluegreenhelper functions', () => {
]
for (const testCase of errorTestCases) {
const spy = vi.spyOn(kubectl, 'getResource')
const spy = jest.spyOn(kubectl, 'getResource')
if (testCase.mockOutput) {
spy.mockImplementation(() => Promise.resolve(testCase.mockOutput))
} else {
spy.mockResolvedValue(null)
spy.mockImplementation()
}
const fetched = await fetchResource(
@@ -232,13 +235,12 @@ describe('bluegreenhelper functions', () => {
stderr: ''
} as ExecOutput
vi.spyOn(kubectl, 'getResource').mockResolvedValue(mockExecOutput)
vi.spyOn(
manifestUpdateUtils,
'UnsetClusterSpecificDetails'
).mockImplementation(() => {
throw new Error('test error')
})
jest.spyOn(kubectl, 'getResource').mockResolvedValue(mockExecOutput)
jest
.spyOn(manifestUpdateUtils, 'UnsetClusterSpecificDetails')
.mockImplementation(() => {
throw new Error('test error')
})
expect(
await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
@@ -265,7 +267,7 @@ describe('bluegreenhelper functions', () => {
describe('deployObjects', () => {
let mockObjects: any[]
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
const mockSuccessResult: ExecOutput = {
stdout: 'deployment.apps/nginx-deployment created',
@@ -283,11 +285,11 @@ describe('bluegreenhelper functions', () => {
// //@ts-ignore
// Kubectl.mockClear()
mockObjects = [testObjects.deploymentEntityList[0]]
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
kubectlApplySpy = jest.spyOn(kubectl, 'apply')
})
afterEach(() => {
vi.clearAllMocks()
jest.clearAllMocks()
})
it('should return execution result and manifest files when kubectl apply succeeds', async () => {
@@ -2,27 +2,27 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {DeployResult} from '../../types/deployResult'
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {
isDeploymentEntity,
isIngressEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes.js'
} from '../../types/kubernetesTypes'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
} from '../../types/blueGreenTypes'
import * as fileHelper from '../../utilities/fileUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
import {
UnsetClusterSpecificDetails,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils.js'
} from '../../utilities/manifestUpdateUtils'
export const GREEN_LABEL_VALUE = 'green'
export const NONE_LABEL_VALUE = 'None'
+52 -52
View File
@@ -1,23 +1,21 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {
deployBlueGreen,
deployBlueGreenIngress,
deployBlueGreenService,
deployBlueGreenSMI
} from './deploy.js'
import * as routeTester from './route.js'
import {Kubectl} from '../../types/kubectl.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import * as bgHelper from './blueGreenHelper.js'
import * as smiHelper from './smiBlueGreenHelper.js'
} from './deploy'
import * as routeTester from './route'
import {Kubectl} from '../../types/kubectl'
import {RouteStrategy} from '../../types/routeStrategy'
import * as TSutils from '../../utilities/trafficSplitUtils'
import * as bgHelper from './blueGreenHelper'
import * as smiHelper from './smiBlueGreenHelper'
import {ExecOutput} from '@actions/exec'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
// Shared variables and mock objects used across all test suites
const mockDeployResult = {
@@ -32,7 +30,7 @@ const mockBgDeployment: BlueGreenDeployment = {
describe('deploy tests', () => {
let kubectl: Kubectl
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
const mockSuccessResult: ExecOutput = {
stdout: 'deployment.apps/nginx-deployment created',
@@ -47,20 +45,21 @@ describe('deploy tests', () => {
}
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
kubectl = new Kubectl('')
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
kubectlApplySpy = jest.spyOn(kubectl, 'apply')
})
test('correctly determines deploy type and acts accordingly', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(() =>
Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockImplementation(() => Promise.resolve(mockBgDeployment))
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
const ingressResult = await deployBlueGreen(
kubectl,
@@ -113,12 +112,12 @@ describe('deploy tests', () => {
fn: () =>
deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.INGRESS),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockImplementation(() => Promise.resolve(mockBgDeployment))
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
}
},
{
@@ -126,24 +125,24 @@ describe('deploy tests', () => {
fn: () =>
deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.SERVICE),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockImplementation(() => Promise.resolve(mockBgDeployment))
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
}
},
{
name: 'should throw error when kubectl apply fails during blue/green deployment with SMI strategy',
fn: () => deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.SMI),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockImplementation(() => Promise.resolve(mockBgDeployment))
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
}
},
{
@@ -182,7 +181,8 @@ describe('deploy timeout tests', () => {
let kubectl: Kubectl
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
kubectl = new Kubectl('')
})
@@ -190,16 +190,16 @@ describe('deploy timeout tests', () => {
const timeout = '300s'
// Mock the helper functions that are actually called
const deployWithLabelSpy = vi
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
const setupSMISpy = vi
const setupSMISpy = jest
.spyOn(smiHelper, 'setupSMI')
.mockResolvedValue(mockBgDeployment)
const routeSpy = vi
const routeSpy = jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockResolvedValue(mockBgDeployment)
@@ -258,10 +258,10 @@ describe('deploy timeout tests', () => {
const timeout = '240s'
// Mock the dependencies
const deployWithLabelSpy = vi
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
@@ -290,10 +290,10 @@ describe('deploy timeout tests', () => {
const timeout = '180s'
// Mock the dependencies
const deployWithLabelSpy = vi
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
@@ -322,13 +322,13 @@ describe('deploy timeout tests', () => {
const timeout = '360s'
// Mock the dependencies
const setupSMISpy = vi
const setupSMISpy = jest
.spyOn(smiHelper, 'setupSMI')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
const deployWithLabelSpy = vi
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
@@ -354,10 +354,10 @@ describe('deploy timeout tests', () => {
})
test('deploy functions without timeout should pass undefined', async () => {
const deployWithLabelSpy = vi
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
+7 -7
View File
@@ -1,23 +1,23 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes.js'
} from '../../types/blueGreenTypes'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {RouteStrategy} from '../../types/routeStrategy'
import {
deployWithLabel,
getManifestObjects,
GREEN_LABEL_VALUE,
deployObjects
} from './blueGreenHelper.js'
import {setupSMI} from './smiBlueGreenHelper.js'
} from './blueGreenHelper'
import {setupSMI} from './smiBlueGreenHelper'
import {routeBlueGreenForDeploy} from './route.js'
import {DeployResult} from '../../types/deployResult.js'
import {routeBlueGreenForDeploy} from './route'
import {DeployResult} from '../../types/deployResult'
export async function deployBlueGreen(
kubectl: Kubectl,
@@ -1,27 +1,27 @@
import {vi} from 'vitest'
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {
getUpdatedBlueGreenIngress,
isIngressRouted,
validateIngresses
} from './ingressBlueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
} from './ingressBlueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kubectl = new Kubectl('')
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
describe('ingress blue green helpers', () => {
let testObjects
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('it should correctly classify ingresses', () => {
@@ -72,7 +72,7 @@ describe('ingress blue green helpers', () => {
test('it should validate ingresses', async () => {
// what if nothing gets returned from fetchResource?
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(null)
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
let validResponse = await validateIngresses(
kubectl,
testObjects.ingressEntityList,
@@ -89,9 +89,9 @@ describe('ingress blue green helpers', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
mockIngress.metadata.labels = mockLabels
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockIngress)
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockIngress))
validResponse = await validateIngresses(
kubectl,
testObjects.ingressEntityList,
@@ -1,12 +1,12 @@
import * as core from '@actions/core'
import {K8sIngress} from '../../types/k8sObject.js'
import {K8sIngress} from '../../types/k8sObject'
import {
addBlueGreenLabelsAndAnnotations,
BLUE_GREEN_VERSION_LABEL,
GREEN_LABEL_VALUE,
fetchResource
} from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
} from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
const BACKEND = 'backend'
+53 -47
View File
@@ -1,22 +1,20 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {getManifestObjects} from './blueGreenHelper.js'
import {getManifestObjects} from './blueGreenHelper'
import {
promoteBlueGreenIngress,
promoteBlueGreenService,
promoteBlueGreenSMI
} from './promote.js'
import {TrafficSplitObject} from '../../types/k8sObject.js'
import * as servicesTester from './serviceBlueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper.js'
import * as smiTester from './smiBlueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
} from './promote'
import {TrafficSplitObject} from '../../types/k8sObject'
import * as servicesTester from './serviceBlueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper'
import * as smiTester from './smiBlueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {ExecOutput} from '@actions/exec'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
// Shared variables used across all test suites
let testObjects: any
@@ -44,12 +42,13 @@ const mockBgDeployment = {
}
describe('promote tests', () => {
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
kubectlApplySpy = jest.spyOn(kubectl, 'apply')
})
test('promote blue/green ingress', async () => {
@@ -58,7 +57,7 @@ describe('promote tests', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
@@ -83,7 +82,7 @@ describe('promote tests', () => {
test('fail to promote invalid blue/green ingress', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
@@ -101,7 +100,7 @@ describe('promote tests', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockLabels},
@@ -121,16 +120,16 @@ describe('promote tests', () => {
test('fail to promote invalid blue/green service', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {},
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
})
)
vi.spyOn(servicesTester, 'validateServicesState').mockImplementationOnce(
() => Promise.resolve(false)
)
jest
.spyOn(servicesTester, 'validateServicesState')
.mockImplementationOnce(() => Promise.resolve(false))
await expect(
promoteBlueGreenService(kubectl, testObjects)
@@ -165,9 +164,9 @@ describe('promote tests', () => {
]
}
}
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsObject)
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsObject))
const deployResult = await promoteBlueGreenSMI(kubectl, testObjects)
@@ -183,11 +182,11 @@ describe('promote tests', () => {
test('promote blue/green SMI with bad trafficsplit', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockImplementation(() =>
Promise.resolve(false)
)
jest
.spyOn(smiTester, 'validateTrafficSplitsState')
.mockImplementation(() => Promise.resolve(false))
await expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrow()
expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrow()
})
// Consolidated error tests
@@ -199,7 +198,7 @@ describe('promote tests', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] =
bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
@@ -215,16 +214,16 @@ describe('promote tests', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] =
bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockLabels},
metadata: {labels: mockLabels, name: 'nginx-service-green'}
})
)
vi.spyOn(servicesTester, 'validateServicesState').mockResolvedValue(
true
)
jest
.spyOn(servicesTester, 'validateServicesState')
.mockResolvedValue(true)
}
},
{
@@ -247,10 +246,12 @@ describe('promote tests', () => {
]
}
}
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(mockTsObject)
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockResolvedValue(
true
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockResolvedValue(mockTsObject)
jest
.spyOn(smiTester, 'validateTrafficSplitsState')
.mockResolvedValue(true)
}
}
])('$name', async ({fn, setup}) => {
@@ -278,12 +279,15 @@ describe('promote tests', () => {
// Timeout tests
describe('promote timeout tests', () => {
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
// @ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
const mockDeployWithLabel = () =>
vi.spyOn(bgHelper, 'deployWithLabel').mockResolvedValue(mockBgDeployment)
jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const setupFetchResource = (
kind: string,
@@ -293,7 +297,7 @@ describe('promote timeout tests', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = labelValue
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue({
jest.spyOn(bgHelper, 'fetchResource').mockResolvedValue({
kind,
spec: {},
metadata: {labels: mockLabels, name}
@@ -326,9 +330,9 @@ describe('promote timeout tests', () => {
'nginx-service-green',
bgHelper.GREEN_LABEL_VALUE
)
vi.spyOn(servicesTester, 'validateServicesState').mockResolvedValue(
true
)
jest
.spyOn(servicesTester, 'validateServicesState')
.mockResolvedValue(true)
}
},
{
@@ -355,10 +359,12 @@ describe('promote timeout tests', () => {
}
}
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(mockTsObject)
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockResolvedValue(
true
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockResolvedValue(mockTsObject)
jest
.spyOn(smiTester, 'validateTrafficSplitsState')
.mockResolvedValue(true)
}
}
])('$name', async ({fn, timeout, setup}) => {
+6 -6
View File
@@ -1,13 +1,13 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper'
import {validateIngresses} from './ingressBlueGreenHelper.js'
import {validateServicesState} from './serviceBlueGreenHelper.js'
import {validateTrafficSplitsState} from './smiBlueGreenHelper.js'
import {validateIngresses} from './ingressBlueGreenHelper'
import {validateServicesState} from './serviceBlueGreenHelper'
import {validateTrafficSplitsState} from './smiBlueGreenHelper'
export async function promoteBlueGreenIngress(
kubectl: Kubectl,
+39 -38
View File
@@ -1,23 +1,21 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {getManifestObjects} from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import {getManifestObjects} from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {
rejectBlueGreenIngress,
rejectBlueGreenService,
rejectBlueGreenSMI
} from './reject.js'
import * as bgHelper from './blueGreenHelper.js'
import * as routeHelper from './route.js'
} from './reject'
import * as bgHelper from './blueGreenHelper'
import * as routeHelper from './route'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kubectl = new Kubectl('')
const TEST_TIMEOUT_SHORT = '60s'
const TEST_TIMEOUT_LONG = '120s'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
// Shared mock objects following DRY principle
const mockSuccessResult = {
@@ -56,13 +54,14 @@ const mockDeleteResult = [
describe('reject tests', () => {
let testObjects: any
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
//@ts-ignore
Kubectl.mockClear()
jest.restoreAllMocks()
testObjects = getManifestObjects(ingressFilepath)
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
kubectlApplySpy = jest.spyOn(kubectl, 'apply')
})
test('reject blue/green ingress', async () => {
@@ -90,13 +89,13 @@ describe('reject tests', () => {
test('reject blue/green ingress with timeout', async () => {
// Mock routeBlueGreenIngressUnchanged and deleteGreenObjects
vi.spyOn(routeHelper, 'routeBlueGreenIngressUnchanged').mockResolvedValue(
mockBgDeployment
)
jest
.spyOn(routeHelper, 'routeBlueGreenIngressUnchanged')
.mockResolvedValue(mockBgDeployment)
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue(mockDeleteResult)
const value = await rejectBlueGreenIngress(
kubectl,
@@ -139,9 +138,9 @@ describe('reject tests', () => {
test('reject blue/green service', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue(mockDeleteResult)
const value = await rejectBlueGreenService(
kubectl,
@@ -164,7 +163,7 @@ describe('reject tests', () => {
test('reject blue/green service with timeout', async () => {
// Mock routeBlueGreenService and deleteGreenObjects
vi.spyOn(routeHelper, 'routeBlueGreenService').mockResolvedValue({
jest.spyOn(routeHelper, 'routeBlueGreenService').mockResolvedValue({
deployResult: {
execResult: {stdout: '', stderr: '', exitCode: 0},
manifestFiles: []
@@ -181,9 +180,11 @@ describe('reject tests', () => {
]
})
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue([
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue([
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
const value = await rejectBlueGreenService(
kubectl,
@@ -212,9 +213,9 @@ describe('reject tests', () => {
// Mock kubectl.apply to return successful result
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
const rejectResult = await rejectBlueGreenSMI(kubectl, testObjects)
expect(rejectResult.deleteResult).toHaveLength(2)
})
@@ -225,27 +226,27 @@ describe('reject tests', () => {
name: 'should throw error when kubectl apply fails during blue/green ingress rejection',
fn: () => rejectBlueGreenIngress(kubectl, testObjects),
setup: () => {
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue(mockDeleteResult)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green service rejection',
fn: () => rejectBlueGreenService(kubectl, testObjects),
setup: () => {
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue(mockDeleteResult)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green SMI rejection',
fn: () => rejectBlueGreenSMI(kubectl, testObjects),
setup: () => {
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
}
}
])('$name', async ({fn, setup}) => {
+7 -7
View File
@@ -1,14 +1,14 @@
import {K8sDeleteObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {K8sDeleteObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {
BlueGreenDeployment,
BlueGreenManifests,
BlueGreenRejectResult
} from '../../types/blueGreenTypes.js'
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper.js'
import {routeBlueGreenSMI} from './route.js'
import {cleanupSMI} from './smiBlueGreenHelper.js'
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route.js'
} from '../../types/blueGreenTypes'
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper'
import {routeBlueGreenSMI} from './route'
import {cleanupSMI} from './smiBlueGreenHelper'
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route'
export async function rejectBlueGreenIngress(
kubectl: Kubectl,
+43 -51
View File
@@ -1,28 +1,24 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {BlueGreenManifests} from '../../types/blueGreenTypes.js'
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {RouteStrategy} from '../../types/routeStrategy'
import {BlueGreenManifests} from '../../types/blueGreenTypes'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE
} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import * as smiHelper from './smiBlueGreenHelper.js'
} from './blueGreenHelper'
import {
routeBlueGreenIngress,
routeBlueGreenService,
routeBlueGreenForDeploy,
routeBlueGreenSMI,
routeBlueGreenIngressUnchanged
} from './route.js'
} from './route'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kc = new Kubectl('')
@@ -41,15 +37,16 @@ const mockFailureResult = {
describe('route function tests', () => {
let testObjects: BlueGreenManifests
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
kubectlApplySpy = vi.spyOn(kc, 'apply')
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
kubectlApplySpy = jest.spyOn(kc, 'apply')
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('correctly prepares blue/green ingresses for deployment', async () => {
@@ -99,9 +96,9 @@ describe('route function tests', () => {
})
test('correctly identifies route pattern and acts accordingly', async () => {
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
const ingressResult = await routeBlueGreenForDeploy(
kc,
@@ -167,9 +164,9 @@ describe('route function tests', () => {
testObjects.serviceEntityList
),
setup: () => {
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
}
},
{
@@ -197,23 +194,24 @@ describe('route timeout tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
afterEach(() => {
vi.restoreAllMocks()
jest.restoreAllMocks()
})
test('routeBlueGreenService with timeout', async () => {
const timeout = '240s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
@@ -239,29 +237,23 @@ describe('route timeout tests', () => {
test('routeBlueGreenSMI with timeout', async () => {
const timeout = '300s'
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
// Mock deployObjects and createTrafficSplitObject to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const createTrafficSplitSpy = vi
.spyOn(smiHelper, 'createTrafficSplitObject')
const createTrafficSplitSpy = jest
.spyOn(require('./smiBlueGreenHelper'), 'createTrafficSplitObject')
.mockResolvedValue({
apiVersion: 'split.smi-spec.io/v1alpha3',
kind: 'TrafficSplit',
metadata: {
name: 'nginx-service-trafficsplit',
labels: new Map(),
annotations: new Map()
},
spec: {service: 'nginx-service', backends: []}
metadata: {name: 'nginx-service-trafficsplit'},
spec: {backends: []}
})
const value = await routeBlueGreenSMI(
@@ -292,8 +284,8 @@ describe('route timeout tests', () => {
const timeout = '180s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
@@ -317,8 +309,8 @@ describe('route timeout tests', () => {
})
test('route functions without timeout should pass undefined', async () => {
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
+10 -10
View File
@@ -1,26 +1,26 @@
import {sleep} from '../../utilities/timeUtils.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {Kubectl} from '../../types/kubectl.js'
import {sleep} from '../../utilities/timeUtils'
import {RouteStrategy} from '../../types/routeStrategy'
import {Kubectl} from '../../types/kubectl'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes.js'
} from '../../types/blueGreenTypes'
import {
getManifestObjects,
GREEN_LABEL_VALUE,
deployObjects
} from './blueGreenHelper.js'
} from './blueGreenHelper'
import {
getUpdatedBlueGreenIngress,
isIngressRouted
} from './ingressBlueGreenHelper.js'
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper.js'
import {createTrafficSplitObject} from './smiBlueGreenHelper.js'
} from './ingressBlueGreenHelper'
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper'
import {createTrafficSplitObject} from './smiBlueGreenHelper'
import * as core from '@actions/core'
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject.js'
import {getBufferTime} from '../../inputUtils.js'
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject'
import {getBufferTime} from '../../inputUtils'
export async function routeBlueGreenForDeploy(
kubectl: Kubectl,
@@ -1,26 +1,26 @@
import {vi} from 'vitest'
import * as core from '@actions/core'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE
} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
} from './blueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import {
getServiceSpecLabel,
getUpdatedBlueGreenService,
validateServicesState
} from './serviceBlueGreenHelper.js'
} from './serviceBlueGreenHelper'
let testObjects
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const kubectl = new Kubectl('')
describe('blue/green service helper tests', () => {
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
@@ -42,7 +42,7 @@ describe('blue/green service helper tests', () => {
mockLabels[BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
const mockSelectors = new Map<string, string>()
mockSelectors[BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockSelectors},
@@ -1,12 +1,12 @@
import * as core from '@actions/core'
import {K8sServiceObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {K8sServiceObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {
addBlueGreenLabelsAndAnnotations,
BLUE_GREEN_VERSION_LABEL,
fetchResource,
GREEN_LABEL_VALUE
} from './blueGreenHelper.js'
} from './blueGreenHelper'
// add green labels to configure existing service
export function getUpdatedBlueGreenService(
@@ -1,16 +1,15 @@
import {vi} from 'vitest'
import {TrafficSplitObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import {TrafficSplitObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {BlueGreenManifests} from '../../types/blueGreenTypes.js'
import {BlueGreenManifests} from '../../types/blueGreenTypes'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE,
NONE_LABEL_VALUE
} from './blueGreenHelper.js'
} from './blueGreenHelper'
import {
cleanupSMI,
@@ -22,10 +21,10 @@ import {
setupSMI,
TRAFFIC_SPLIT_OBJECT,
validateTrafficSplitsState
} from './smiBlueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
} from './smiBlueGreenHelper'
import * as bgHelper from './blueGreenHelper'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const kc = new Kubectl('')
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
@@ -69,20 +68,21 @@ const mockTsObject: TrafficSplitObject = {
describe('SMI Helper tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
//@ts-ignore
Kubectl.mockClear()
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('')
)
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve(''))
testObjects = getManifestObjects(ingressFilepath)
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('setupSMI tests', async () => {
vi.spyOn(kc, 'apply').mockResolvedValue(mockSuccessResult)
jest.spyOn(kc, 'apply').mockResolvedValue(mockSuccessResult)
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
@@ -174,9 +174,9 @@ describe('SMI Helper tests', () => {
})
test('validateTrafficSplitsState', async () => {
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsObject)
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsObject))
let valResult = await validateTrafficSplitsState(
kc,
@@ -187,9 +187,9 @@ describe('SMI Helper tests', () => {
const mockTsCopy = JSON.parse(JSON.stringify(mockTsObject))
mockTsCopy.spec.backends[0].weight = MAX_VAL
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsCopy)
)
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsCopy))
valResult = await validateTrafficSplitsState(
kc,
@@ -197,7 +197,7 @@ describe('SMI Helper tests', () => {
)
expect(valResult).toBe(false)
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(null)
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
valResult = await validateTrafficSplitsState(
kc,
testObjects.serviceEntityList
@@ -218,7 +218,7 @@ describe('SMI Helper tests', () => {
name: 'should throw error when kubectl apply fails during SMI setup',
fn: () => setupSMI(kc, testObjects.serviceEntityList),
setup: () => {
vi.spyOn(kc, 'apply').mockResolvedValue(mockFailureResult)
jest.spyOn(kc, 'apply').mockResolvedValue(mockFailureResult)
}
}
])('$name', async ({fn, setup}) => {
@@ -229,7 +229,7 @@ describe('SMI Helper tests', () => {
// Timeout-specific tests
test('setupSMI with timeout test', async () => {
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
@@ -257,7 +257,7 @@ describe('SMI Helper tests', () => {
})
test('createTrafficSplitObject with timeout test', async () => {
const deleteObjectsSpy = vi
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
@@ -288,7 +288,7 @@ describe('SMI Helper tests', () => {
})
test('createTrafficSplitObject with GREEN_LABEL_VALUE and timeout test', async () => {
const deleteObjectsSpy = vi
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
@@ -321,7 +321,7 @@ describe('SMI Helper tests', () => {
})
test('cleanupSMI with timeout test', async () => {
const deleteObjectsSpy = vi
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
@@ -352,7 +352,7 @@ describe('SMI Helper tests', () => {
})
test('setupSMI without timeout test', async () => {
const deployObjectsSpy = vi
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
@@ -375,7 +375,7 @@ describe('SMI Helper tests', () => {
})
test('createTrafficSplitObject without timeout test', async () => {
const deleteObjectsSpy = vi
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
@@ -398,7 +398,7 @@ describe('SMI Helper tests', () => {
})
test('cleanupSMI without timeout test', async () => {
const deleteObjectsSpy = vi
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl.js'
import * as kubectlUtils from '../../utilities/trafficSplitUtils.js'
import {Kubectl} from '../../types/kubectl'
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import {
deleteObjects,
deployObjects,
@@ -11,15 +11,15 @@ import {
GREEN_SUFFIX,
NONE_LABEL_VALUE,
STABLE_SUFFIX
} from './blueGreenHelper.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {
K8sDeleteObject,
K8sObject,
TrafficSplitObject
} from '../../types/k8sObject.js'
import {DeployResult} from '../../types/deployResult.js'
import {inputAnnotations} from '../../inputUtils.js'
} from '../../types/k8sObject'
import {DeployResult} from '../../types/deployResult'
import {inputAnnotations} from '../../inputUtils'
export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
+6 -6
View File
@@ -1,4 +1,4 @@
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as core from '@actions/core'
@@ -7,15 +7,15 @@ import {
isDeploymentEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes.js'
import * as utils from '../../utilities/manifestUpdateUtils.js'
} from '../../types/kubernetesTypes'
import * as utils from '../../utilities/manifestUpdateUtils'
import {
updateObjectAnnotations,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils.js'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
} from '../../utilities/manifestUpdateUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
export const CANARY_VERSION_LABEL = 'workflow/version'
const BASELINE_SUFFIX = '-baseline'
@@ -1,15 +1,11 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core')
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import {
deployPodCanary,
calculateReplicaCountForCanary
} from './podCanaryHelper.js'
} from './podCanaryHelper'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const kc = new Kubectl('')
@@ -39,17 +35,18 @@ const TIMEOUT_300S = '300s'
describe('Pod Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
//@ts-ignore
Kubectl.mockClear()
jest.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = vi.spyOn(kc, 'apply')
kubectlApplySpy = jest.spyOn(kc, 'apply')
// Mock core.getInput with default values
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -64,7 +61,7 @@ describe('Pod Canary Helper tests', () => {
})
afterEach(() => {
vi.restoreAllMocks()
jest.restoreAllMocks()
kubectlApplySpy.mockClear()
})
@@ -117,7 +114,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should throw error for invalid low percentage', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_LOW_PERCENTAGE.toString()
return ''
})
@@ -130,7 +127,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should throw error for invalid high percentage', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_HIGH_PERCENTAGE.toString()
return ''
})
@@ -146,7 +143,7 @@ describe('Pod Canary Helper tests', () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
// Test minimum valid percentage
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MIN_PERCENTAGE.toString()
return ''
})
@@ -155,7 +152,7 @@ describe('Pod Canary Helper tests', () => {
expect(resultMin.execResult).toEqual(mockSuccessResult)
// Test maximum valid percentage
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MAX_PERCENTAGE.toString()
return ''
})
@@ -165,7 +162,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should handle force deployment option', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -189,7 +186,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should handle server-side apply option', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -1,15 +1,15 @@
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as canaryDeploymentHelper from './canaryHelper.js'
import {isDeploymentEntity} from '../../types/kubernetesTypes.js'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils.js'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject} from '../../types/k8sObject.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
import * as fileHelper from '../../utilities/fileUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity} from '../../types/kubernetesTypes'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
import {DeployResult} from '../../types/deployResult'
import {K8sObject} from '../../types/k8sObject'
import {checkForErrors} from '../../utilities/kubectlUtils'
export async function deployPodCanary(
filePaths: string[],
@@ -1,34 +1,13 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
getInput: vi.fn().mockReturnValue(''),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
group: vi
.fn()
.mockImplementation(
async (_name: string, fn: () => Promise<void>) => await fn()
)
}
})
import * as core from '@actions/core'
import * as fs from 'fs'
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import {
deploySMICanary,
redirectTrafficToCanaryDeployment,
redirectTrafficToStableDeployment
} from './smiCanaryHelper.js'
} from './smiCanaryHelper'
vi.mock('../../types/kubectl')
jest.mock('../../types/kubectl')
const kc = new Kubectl('')
@@ -61,21 +40,22 @@ const TIMEOUT_240S = '240s'
describe('SMI Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: MockInstance
let kubectlExecuteCommandSpy: MockInstance
let kubectlApplySpy: jest.SpyInstance
let kubectlExecuteCommandSpy: jest.SpyInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
//@ts-ignore
Kubectl.mockClear()
jest.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = vi.spyOn(kc, 'apply')
kubectlExecuteCommandSpy = vi
kubectlApplySpy = jest.spyOn(kc, 'apply')
kubectlExecuteCommandSpy = jest
.spyOn(kc, 'executeCommand')
.mockResolvedValue(mockExecuteCommandResult)
// Mock core.getInput with default values
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return '50'
@@ -92,7 +72,7 @@ describe('SMI Canary Helper tests', () => {
})
afterEach(() => {
vi.restoreAllMocks()
jest.restoreAllMocks()
kubectlApplySpy.mockClear()
})
@@ -126,7 +106,7 @@ describe('SMI Canary Helper tests', () => {
})
test('should handle custom replica count from input', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'baseline-and-canary-replicas':
return VALID_REPLICA_COUNT.toString()
+10 -13
View File
@@ -1,20 +1,17 @@
import {Kubectl} from '../../types/kubectl.js'
import {Kubectl} from '../../types/kubectl'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as kubectlUtils from '../../utilities/trafficSplitUtils.js'
import * as canaryDeploymentHelper from './canaryHelper.js'
import * as podCanaryHelper from './podCanaryHelper.js'
import {
isDeploymentEntity,
isServiceEntity
} from '../../types/kubernetesTypes.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
import {inputAnnotations} from '../../inputUtils.js'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject} from '../../types/k8sObject.js'
import * as fileHelper from '../../utilities/fileUtils'
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import * as podCanaryHelper from './podCanaryHelper'
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
import {checkForErrors} from '../../utilities/kubectlUtils'
import {inputAnnotations} from '../../inputUtils'
import {DeployResult} from '../../types/deployResult'
import {K8sObject} from '../../types/k8sObject'
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
+19 -19
View File
@@ -1,41 +1,41 @@
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as canaryDeploymentHelper from './canary/canaryHelper.js'
import * as models from '../types/kubernetesTypes.js'
import {isDeploymentEntity} from '../types/kubernetesTypes.js'
import * as fileHelper from '../utilities/fileUtils.js'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import * as canaryDeploymentHelper from './canary/canaryHelper'
import * as models from '../types/kubernetesTypes'
import {isDeploymentEntity} from '../types/kubernetesTypes'
import * as fileHelper from '../utilities/fileUtils'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
import {Kubectl, Resource} from '../types/kubectl'
import {deployPodCanary} from './canary/podCanaryHelper.js'
import {deploySMICanary} from './canary/smiCanaryHelper.js'
import {DeploymentConfig} from '../types/deploymentConfig.js'
import {deployBlueGreen} from './blueGreen/deploy.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {deployPodCanary} from './canary/podCanaryHelper'
import {deploySMICanary} from './canary/smiCanaryHelper'
import {DeploymentConfig} from '../types/deploymentConfig'
import {deployBlueGreen} from './blueGreen/deploy'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import * as core from '@actions/core'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy} from '../types/routeStrategy.js'
} from '../types/trafficSplitMethod'
import {parseRouteStrategy} from '../types/routeStrategy'
import {ExecOutput} from '@actions/exec'
import {
getWorkflowAnnotationKeyLabel,
getWorkflowAnnotations,
cleanLabel
} from '../utilities/workflowAnnotationUtils.js'
} from '../utilities/workflowAnnotationUtils'
import {
annotateChildPods,
checkForErrors,
getLastSuccessfulRunSha
} from '../utilities/kubectlUtils.js'
} from '../utilities/kubectlUtils'
import {
getWorkflowFilePath,
normalizeWorkflowStrLabel
} from '../utilities/githubUtils.js'
import {getDeploymentConfig} from '../utilities/dockerUtils.js'
import {DeployResult} from '../types/deployResult.js'
import {ClusterType} from '../inputUtils.js'
} from '../utilities/githubUtils'
import {getDeploymentConfig} from '../utilities/dockerUtils'
import {DeployResult} from '../types/deployResult'
import {ClusterType} from '../inputUtils'
export async function deployManifests(
files: string[],
+1 -1
View File
@@ -1,4 +1,4 @@
import {Action, parseAction} from './action.js'
import {Action, parseAction} from './action'
describe('Action type', () => {
test('it has required values', () => {
+2 -2
View File
@@ -1,5 +1,5 @@
import {DeployResult} from './deployResult.js'
import {K8sObject, K8sDeleteObject} from './k8sObject.js'
import {DeployResult} from './deployResult'
import {K8sObject, K8sDeleteObject} from './k8sObject'
export interface BlueGreenDeployment {
deployResult: DeployResult
+1 -4
View File
@@ -1,7 +1,4 @@
import {
DeploymentStrategy,
parseDeploymentStrategy
} from './deploymentStrategy.js'
import {DeploymentStrategy, parseDeploymentStrategy} from './deploymentStrategy'
describe('Deployment strategy type', () => {
test('it has required values', () => {
+4 -7
View File
@@ -1,7 +1,4 @@
import {vi} from 'vitest'
vi.mock('@actions/exec')
import {DockerExec} from './docker.js'
import {DockerExec} from './docker'
import * as actions from '@actions/exec'
const dockerPath = 'dockerPath'
@@ -15,7 +12,7 @@ describe('Docker class', () => {
const execReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})
@@ -63,7 +60,7 @@ describe('Docker class', () => {
const execReturn = {exitCode: 3, stdout: '', stderr: ''}
beforeEach(() => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})
@@ -83,7 +80,7 @@ describe('Docker class', () => {
const execReturn = {exitCode: 0, stdout: '', stderr: 'Output'}
beforeEach(() => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})
+12 -18
View File
@@ -1,10 +1,4 @@
import {vi} from 'vitest'
vi.mock('@actions/exec')
vi.mock('@actions/io')
vi.mock('@actions/core')
vi.mock('@actions/tool-cache')
import {getKubectlPath, Kubectl} from './kubectl.js'
import {getKubectlPath, Kubectl} from './kubectl'
import * as exec from '@actions/exec'
import * as io from '@actions/io'
import * as core from '@actions/core'
@@ -15,27 +9,27 @@ describe('Kubectl path', () => {
const path = 'path'
it('gets the kubectl path', async () => {
vi.spyOn(core, 'getInput').mockImplementationOnce(() => '')
vi.spyOn(io, 'which').mockImplementationOnce(async () => path)
jest.spyOn(core, 'getInput').mockImplementationOnce(() => '')
jest.spyOn(io, 'which').mockImplementationOnce(async () => path)
expect(await getKubectlPath()).toBe(path)
})
it('gets the kubectl path with version', async () => {
vi.spyOn(core, 'getInput').mockImplementationOnce(() => version)
vi.spyOn(toolCache, 'find').mockImplementationOnce(() => path)
jest.spyOn(core, 'getInput').mockImplementationOnce(() => version)
jest.spyOn(toolCache, 'find').mockImplementationOnce(() => path)
expect(await getKubectlPath()).toBe(path)
})
it('throws if kubectl not found', async () => {
// without version
vi.spyOn(io, 'which').mockImplementationOnce(async () => '')
jest.spyOn(io, 'which').mockImplementationOnce(async () => '')
await expect(() => getKubectlPath()).rejects.toThrow()
// with verision
vi.spyOn(core, 'getInput').mockImplementationOnce(() => '')
vi.spyOn(io, 'which').mockImplementationOnce(async () => '')
jest.spyOn(core, 'getInput').mockImplementationOnce(() => '')
jest.spyOn(io, 'which').mockImplementationOnce(async () => '')
await expect(() => getKubectlPath()).rejects.toThrow()
})
})
@@ -52,7 +46,7 @@ describe('Kubectl class', () => {
const mockExecReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return mockExecReturn
})
})
@@ -643,7 +637,7 @@ describe('Kubectl class', () => {
stderr: ''
}
vi.spyOn(exec, 'getExecOutput').mockImplementationOnce(async () => {
jest.spyOn(exec, 'getExecOutput').mockImplementationOnce(async () => {
return describeReturn
})
@@ -656,7 +650,7 @@ describe('Kubectl class', () => {
const skipTls = true
const kubectl = new Kubectl(kubectlPath, testNamespace, skipTls)
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return {exitCode: 0, stderr: '', stdout: ''}
})
@@ -683,7 +677,7 @@ describe('Kubectl namespace handling', () => {
const execReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
vi.spyOn(exec, 'getExecOutput').mockResolvedValue(execReturn)
jest.spyOn(exec, 'getExecOutput').mockResolvedValue(execReturn)
})
const runApply = async (namespace?: string) => {
+2 -6
View File
@@ -1,5 +1,5 @@
import {ExecOutput, getExecOutput} from '@actions/exec'
import {createInlineArray} from '../utilities/arrayUtils.js'
import {createInlineArray} from '../utilities/arrayUtils'
import * as core from '@actions/core'
import * as toolCache from '@actions/tool-cache'
import * as io from '@actions/io'
@@ -238,17 +238,13 @@ export class Kubectl {
})
}
public getNamespace(namespaceOverride?: string): string {
return namespaceOverride || this.namespace
}
protected getFlags(namespaceOverride?: string): string[] {
const flags = []
if (this.ignoreSSLErrors) {
flags.push('--insecure-skip-tls-verify')
}
const ns = this.getNamespace(namespaceOverride)
const ns = namespaceOverride || this.namespace
if (ns) {
flags.push('--namespace', ns)
}
+1 -1
View File
@@ -10,7 +10,7 @@ import {
ServiceTypes,
WORKLOAD_TYPES,
WORKLOAD_TYPES_WITH_ROLLOUT_STATUS
} from './kubernetesTypes.js'
} from './kubernetesTypes'
describe('Kubernetes types', () => {
it('contains kubernetes workloads', () => {
+6 -9
View File
@@ -1,13 +1,10 @@
import {vi} from 'vitest'
vi.mock('@actions/exec')
import * as fileUtils from '../utilities/fileUtils.js'
import * as fileUtils from '../utilities/fileUtils'
import fs from 'node:fs'
import {
PrivateKubectl,
extractFileNames,
replaceFileNamesWithShallowNamesRelativeToTemp
} from './privatekubectl.js'
} from './privatekubectl'
import * as exec from '@actions/exec'
describe('Private kubectl', () => {
@@ -20,14 +17,14 @@ describe('Private kubectl', () => {
'resourceName'
)
const spy = vi
const spy = jest
.spyOn(fileUtils, 'getTempDirectory')
.mockImplementation(() => {
return '/tmp'
})
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})
@@ -51,7 +48,7 @@ describe('Private kubectl', () => {
test('Should throw well defined Error on error from Azure', async () => {
const errorMsg = 'An error message'
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return {exitCode: 1, stdout: '', stderr: errorMsg}
})
+2 -2
View File
@@ -1,10 +1,10 @@
import {Kubectl} from './kubectl.js'
import {Kubectl} from './kubectl'
import minimist from 'minimist'
import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec'
import * as core from '@actions/core'
import fs from 'node:fs'
import * as path from 'path'
import {getTempDirectory} from '../utilities/fileUtils.js'
import {getTempDirectory} from '../utilities/fileUtils'
export class PrivateKubectl extends Kubectl {
protected async execute(args: string[], silent: boolean = false) {
+1 -1
View File
@@ -1,4 +1,4 @@
import {parseRouteStrategy, RouteStrategy} from './routeStrategy.js'
import {parseRouteStrategy, RouteStrategy} from './routeStrategy'
describe('Route strategy type', () => {
test('it has required values', () => {
+1 -4
View File
@@ -1,7 +1,4 @@
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from './trafficSplitMethod.js'
import {parseTrafficSplitMethod, TrafficSplitMethod} from './trafficSplitMethod'
describe('Traffic split method type', () => {
test('it has required values', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import {createInlineArray} from './arrayUtils.js'
import {createInlineArray} from './arrayUtils'
describe('array utilities', () => {
it('creates an inline array', () => {
+3 -6
View File
@@ -1,18 +1,15 @@
import {vi} from 'vitest'
vi.mock('@actions/io')
import * as io from '@actions/io'
import {checkDockerPath} from './dockerUtils.js'
import {checkDockerPath} from './dockerUtils'
describe('docker utilities', () => {
it('checks if docker is installed', async () => {
// docker installed
const path = 'path'
vi.spyOn(io, 'which').mockImplementationOnce(async () => path)
jest.spyOn(io, 'which').mockImplementationOnce(async () => path)
expect(() => checkDockerPath()).not.toThrow()
// docker not installed
vi.spyOn(io, 'which').mockImplementationOnce(async () => {
jest.spyOn(io, 'which').mockImplementationOnce(async () => {
throw new Error('not found')
})
await expect(() => checkDockerPath()).rejects.toThrow()
+3 -3
View File
@@ -1,8 +1,8 @@
import * as io from '@actions/io'
import {DeploymentConfig} from '../types/deploymentConfig.js'
import {DeploymentConfig} from '../types/deploymentConfig'
import * as core from '@actions/core'
import {DockerExec} from '../types/docker.js'
import {getNormalizedPath} from './githubUtils.js'
import {DockerExec} from '../types/docker'
import {getNormalizedPath} from './githubUtils'
export async function getDeploymentConfig(): Promise<DeploymentConfig> {
let helmChartPaths: string[] =
+5 -6
View File
@@ -1,10 +1,9 @@
import {vi, type Mocked} from 'vitest'
import {parseDuration} from './durationUtils.js'
import {parseDuration} from './durationUtils'
import * as core from '@actions/core'
// Mock core.debug
vi.mock('@actions/core')
const mockCore = core as Mocked<typeof core>
jest.mock('@actions/core')
const mockCore = core as jest.Mocked<typeof core>
// Test data constants
const VALID_TIMEOUTS = {
@@ -47,7 +46,7 @@ const expectInvalidTimeout = (input: string, expectedError: string) => {
describe('validateTimeoutDuration', () => {
beforeEach(() => {
vi.clearAllMocks()
jest.clearAllMocks()
})
describe('valid timeout formats', () => {
@@ -91,7 +90,7 @@ describe('validateTimeoutDuration', () => {
'No unit specified for timeout "5", assuming minutes'
)
vi.clearAllMocks()
jest.clearAllMocks()
parseDuration('30s')
expect(mockCore.debug).not.toHaveBeenCalled()
+14 -365
View File
@@ -1,18 +1,13 @@
import {vi} from 'vitest'
import * as fileUtils from './fileUtils.js'
import * as fileUtils from './fileUtils'
import * as yaml from 'js-yaml'
import fs from 'node:fs'
import os from 'node:os'
import * as path from 'path'
import {K8sObject} from '../types/k8sObject.js'
import {K8sObject} from '../types/k8sObject'
const sampleYamlUrl =
'https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/nginx-deployment.yaml'
describe('File utils', () => {
beforeAll(() => {
process.env.GITHUB_WORKSPACE ??= process.cwd()
})
test('correctly parses a yaml file from a URL', async () => {
const tempFile = await fileUtils.writeYamlFromURLToFile(sampleYamlUrl, 0)
const fileContents = fs.readFileSync(tempFile).toString()
@@ -57,7 +52,7 @@ describe('File utils', () => {
expect(testSearch).toHaveLength(10)
expectedManifests.forEach((fileName) => {
if (fileName.startsWith('test/unit')) {
expect(testSearch).toContain(path.resolve(fileName))
expect(testSearch).toContain(fileName)
} else {
expect(fileName.includes(fileUtils.urlFileKind)).toBe(true)
expect(fileName.startsWith(fileUtils.getTempDirectory()))
@@ -74,7 +69,7 @@ describe('File utils', () => {
'manifest_test_dir'
)
await expect(
expect(
fileUtils.getFilesFromDirectoriesAndURLs([badPath, goodPath])
).rejects.toThrow()
})
@@ -109,366 +104,20 @@ describe('File utils', () => {
fileUtils.writeYamlFromURLToFile(badUrl, 0)
).rejects.toBeTruthy()
})
it('rejects manifest inputs that resolve outside the workspace', async () => {
const originalWs = process.env.GITHUB_WORKSPACE
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-'))
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'))
fs.writeFileSync(path.join(outside, 'secrets.yaml'), 'api_key: x')
process.env.GITHUB_WORKSPACE = ws
try {
await expect(
fileUtils.getFilesFromDirectoriesAndURLs([outside])
).rejects.toThrow(/outside the workspace/)
await expect(
fileUtils.getFilesFromDirectoriesAndURLs([
path.join(outside, 'secrets.yaml')
])
).rejects.toThrow(/outside the workspace/)
} finally {
if (originalWs === undefined) delete process.env.GITHUB_WORKSPACE
else process.env.GITHUB_WORKSPACE = originalWs
fs.rmSync(ws, {recursive: true, force: true})
fs.rmSync(outside, {recursive: true, force: true})
}
})
it('rejects symlinks inside a directory that escape the workspace', async () => {
const originalWs = process.env.GITHUB_WORKSPACE
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-'))
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'))
const escapeTarget = path.join(outside, 'passwd.yaml')
fs.writeFileSync(escapeTarget, 'root:x:0:0')
const dir = path.join(ws, 'manifests')
fs.mkdirSync(dir)
fs.symlinkSync(escapeTarget, path.join(dir, 'escape.yaml'))
process.env.GITHUB_WORKSPACE = ws
try {
await expect(
fileUtils.getFilesFromDirectoriesAndURLs([dir])
).rejects.toThrow(/outside the workspace/)
} finally {
if (originalWs === undefined) delete process.env.GITHUB_WORKSPACE
else process.env.GITHUB_WORKSPACE = originalWs
fs.rmSync(ws, {recursive: true, force: true})
fs.rmSync(outside, {recursive: true, force: true})
}
})
})
describe('moveFileToTmpDir', () => {
let workspace: string
let originalWorkspace: string | undefined
let originalTemp: string | undefined
let originalCwd: string
let tmpDir: string
describe('moving files to temp', () => {
it('correctly moves the contents of a file to the temporary directory', () => {
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})
const originalFilePath = path.join('path', 'in', 'repo')
beforeEach(() => {
originalWorkspace = process.env.GITHUB_WORKSPACE
originalTemp = process.env.RUNNER_TEMP
originalCwd = process.cwd()
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-'))
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rt-'))
process.env.GITHUB_WORKSPACE = workspace
process.env.RUNNER_TEMP = tmpDir
})
const output = fileUtils.moveFileToTmpDir(originalFilePath)
afterEach(() => {
process.chdir(originalCwd)
if (originalWorkspace === undefined) delete process.env.GITHUB_WORKSPACE
else process.env.GITHUB_WORKSPACE = originalWorkspace
if (originalTemp === undefined) delete process.env.RUNNER_TEMP
else process.env.RUNNER_TEMP = originalTemp
fs.rmSync(workspace, {recursive: true, force: true})
fs.rmSync(tmpDir, {recursive: true, force: true})
})
it('copies a workspace file to RUNNER_TEMP using a basename-only destination', () => {
const src = path.join(workspace, 'svc.yaml')
fs.writeFileSync(src, 'kind: Service')
const out = fileUtils.moveFileToTmpDir(src)
expect(fs.realpathSync(path.dirname(out))).toBe(fs.realpathSync(tmpDir))
expect(path.basename(out)).toMatch(/^svc_\d+_\d+\.yaml$/)
expect(fs.readFileSync(out).toString()).toBe('kind: Service')
})
it('rejects relative traversal that escapes the workspace', () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'))
fs.writeFileSync(path.join(outside, 'secrets.yaml'), 'api_key: x')
process.chdir(workspace)
const rel = path.relative(workspace, path.join(outside, 'secrets.yaml'))
expect(() => fileUtils.moveFileToTmpDir(rel)).toThrow(
/outside the workspace/
expect(output).toEqual(
path.join(fileUtils.getTempDirectory(), '/path/in/repo')
)
fs.rmSync(outside, {recursive: true, force: true})
})
it('does not collide when two inputs share a basename', () => {
const a = path.join(workspace, 'a')
const b = path.join(workspace, 'b')
fs.mkdirSync(a)
fs.mkdirSync(b)
fs.writeFileSync(path.join(a, 'svc.yaml'), 'A')
fs.writeFileSync(path.join(b, 'svc.yaml'), 'B')
const outA = fileUtils.moveFileToTmpDir(path.join(a, 'svc.yaml'))
const outB = fileUtils.moveFileToTmpDir(path.join(b, 'svc.yaml'))
expect(outA).not.toBe(outB)
expect(fs.readFileSync(outA).toString()).toBe('A')
expect(fs.readFileSync(outB).toString()).toBe('B')
})
})
describe('assertPathWithinWorkspace', () => {
let workspace: string
let outside: string
let originalWorkspace: string | undefined
let originalCwd: string
beforeEach(() => {
originalWorkspace = process.env.GITHUB_WORKSPACE
originalCwd = process.cwd()
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-'))
outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'))
process.env.GITHUB_WORKSPACE = workspace
})
afterEach(() => {
process.chdir(originalCwd)
if (originalWorkspace === undefined) {
delete process.env.GITHUB_WORKSPACE
} else {
process.env.GITHUB_WORKSPACE = originalWorkspace
}
fs.rmSync(workspace, {recursive: true, force: true})
fs.rmSync(outside, {recursive: true, force: true})
})
it('returns the resolved path for files inside the workspace', () => {
const inside = path.join(workspace, 'a.yaml')
fs.writeFileSync(inside, 'kind: X')
const result = fileUtils.assertPathWithinWorkspace(inside)
expect(result).toBe(fs.realpathSync(inside))
})
it('accepts workspace files whose basename starts with ..', () => {
const inside = path.join(workspace, '..bar.yaml')
fs.writeFileSync(inside, 'kind: X')
expect(fileUtils.assertPathWithinWorkspace(inside)).toBe(
fs.realpathSync(inside)
)
})
it('throws for relative traversal paths that escape the workspace', () => {
const target = path.join(outside, 'secrets.yaml')
fs.writeFileSync(target, 'api_key: secret')
const rel = path.relative(workspace, target)
process.chdir(workspace)
expect(() => fileUtils.assertPathWithinWorkspace(rel)).toThrow(
/outside the workspace/
)
})
it('resolves relative paths against GITHUB_WORKSPACE even when CWD differs', () => {
const inside = path.join(workspace, 'manifest.yaml')
fs.writeFileSync(inside, 'kind: X')
// Deliberately chdir somewhere unrelated so a process.cwd()-based
// resolver would either reject or resolve to the wrong place.
process.chdir(os.tmpdir())
const result = fileUtils.assertPathWithinWorkspace('manifest.yaml')
expect(result).toBe(fs.realpathSync(inside))
})
it('throws for absolute paths outside the workspace', () => {
const target = path.join(outside, 'secrets.yaml')
fs.writeFileSync(target, 'api_key: secret')
expect(() => fileUtils.assertPathWithinWorkspace(target)).toThrow(
/outside the workspace/
)
})
it('throws when a symlink inside the workspace points outside', () => {
const target = path.join(outside, 'secrets.yaml')
fs.writeFileSync(target, 'api_key: secret')
const link = path.join(workspace, 'evil.yaml')
fs.symlinkSync(target, link)
expect(() => fileUtils.assertPathWithinWorkspace(link)).toThrow(
/outside the workspace/
)
})
it('throws a clear error for missing files', () => {
const missing = path.join(workspace, 'nope.yaml')
expect(() => fileUtils.assertPathWithinWorkspace(missing)).toThrow(
/does not exist or is not readable/
)
})
it('skips containment when GITHUB_WORKSPACE is unset', () => {
delete process.env.GITHUB_WORKSPACE
const target = path.join(outside, 'whatever.yaml')
fs.writeFileSync(target, 'kind: X')
expect(fileUtils.assertPathWithinWorkspace(target)).toBe(target)
})
})
import {EventEmitter} from 'node:events'
import {PassThrough} from 'node:stream'
import * as https from 'node:https'
const httpsState = vi.hoisted(() => ({impl: null as any}))
vi.mock('https', async (importOriginal) => {
const actual = await importOriginal<typeof import('https')>()
const get = (...args: any[]) =>
httpsState.impl ? httpsState.impl(...args) : (actual.get as any)(...args)
return {
...actual,
default: {...actual, get},
get
}
})
describe('writeYamlFromURLToFile error handling', () => {
let tempDir: string
let originalRunnerTemp: string | undefined
beforeEach(() => {
originalRunnerTemp = process.env.RUNNER_TEMP
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'url-fetch-'))
process.env.RUNNER_TEMP = tempDir
})
afterEach(() => {
httpsState.impl = null
vi.restoreAllMocks()
if (originalRunnerTemp === undefined) {
delete process.env.RUNNER_TEMP
} else {
process.env.RUNNER_TEMP = originalRunnerTemp
}
fs.rmSync(tempDir, {recursive: true, force: true})
})
// Wait one tick so cleanupAndReject's async fs.rm callback can fire
// before the test inspects the temp directory.
const waitForCleanup = () =>
new Promise<void>((r) => setImmediate(() => setImmediate(r)))
function mockHttpsGet(
makeResponse: () => {
response: EventEmitter & {
statusCode?: number
statusMessage?: string
pipe: PassThrough['pipe']
resume: () => void
}
requestEmitter: EventEmitter
}
) {
httpsState.impl = ((url: string, cb?: any) => {
const {response, requestEmitter} = makeResponse()
if (cb) setImmediate(() => cb(response))
return requestEmitter as any
}) as any
}
it('rejects on HTTP 500 without writing a file', async () => {
const requestEmitter = new EventEmitter()
const response = Object.assign(new PassThrough(), {
statusCode: 500,
statusMessage: 'Server Error',
resume() {
/* drain */
}
})
mockHttpsGet(() => ({response: response as any, requestEmitter}))
await expect(
fileUtils.writeYamlFromURLToFile('https://example.com/x.yaml', 99)
).rejects.toThrow(/Server Error/)
})
it('rejects when the response stream errors mid-download', async () => {
const requestEmitter = new EventEmitter()
const response = Object.assign(new PassThrough(), {
statusCode: 200,
statusMessage: 'OK',
resume() {}
})
mockHttpsGet(() => ({response: response as any, requestEmitter}))
const p = fileUtils.writeYamlFromURLToFile(
'https://example.com/y.yaml',
100
)
setImmediate(() => response.emit('error', new Error('socket reset')))
await expect(p).rejects.toThrow(/socket reset/)
})
it('rejects on request-level errors', async () => {
const requestEmitter = new EventEmitter()
const response = Object.assign(new PassThrough(), {
statusCode: 200,
resume() {}
})
mockHttpsGet(() => ({response: response as any, requestEmitter}))
const p = fileUtils.writeYamlFromURLToFile(
'https://example.com/z.yaml',
101
)
setImmediate(() => requestEmitter.emit('error', new Error('DNS failure')))
await expect(p).rejects.toThrow(/DNS failure/)
})
it('removes temp file when verification fails', async () => {
const requestEmitter = new EventEmitter()
const response = Object.assign(new PassThrough(), {
statusCode: 200,
statusMessage: 'OK'
})
mockHttpsGet(() => ({response: response as any, requestEmitter}))
const before = new Set(fs.readdirSync(tempDir))
const p = fileUtils.writeYamlFromURLToFile(
'https://example.com/bad.yaml',
200
)
// Stream a YAML document missing required k8s fields so verifyYaml fails.
setImmediate(() => {
response.end('not: a-real-manifest\n')
})
await expect(p).rejects.toThrow(/missing fields|failed to parse/)
await waitForCleanup()
const after = fs.readdirSync(tempDir).filter((f) => !before.has(f))
expect(after).toEqual([])
})
it('removes temp file on mid-stream response error', async () => {
const requestEmitter = new EventEmitter()
const response = Object.assign(new PassThrough(), {
statusCode: 200,
statusMessage: 'OK',
resume() {}
})
mockHttpsGet(() => ({response: response as any, requestEmitter}))
const before = new Set(fs.readdirSync(tempDir))
const p = fileUtils.writeYamlFromURLToFile(
'https://example.com/midstream.yaml',
201
)
setImmediate(() => {
response.write('kind: Foo\n')
response.emit('error', new Error('socket reset'))
})
await expect(p).rejects.toThrow(/socket reset/)
await waitForCleanup()
const after = fs.readdirSync(tempDir).filter((f) => !before.has(f))
expect(after).toEqual([])
})
})
+32 -91
View File
@@ -4,63 +4,17 @@ import * as path from 'path'
import * as core from '@actions/core'
import * as os from 'os'
import * as yaml from 'js-yaml'
import {Errorable, succeeded, failed, Failed} from '../types/errorable.js'
import {getCurrentTime} from './timeUtils.js'
import {isHttpUrl} from './githubUtils.js'
import {K8sObject} from '../types/k8sObject.js'
import {Errorable, succeeded, failed, Failed} from '../types/errorable'
import {getCurrentTime} from './timeUtils'
import {isHttpUrl} from './githubUtils'
import {K8sObject} from '../types/k8sObject'
export const urlFileKind = 'urlfile'
let moveCounter = 0
export function getTempDirectory(): string {
return process.env['RUNNER_TEMP'] || os.tmpdir()
}
// Exported for tests. Validates that `inputPath` resolves (after symlink
// resolution) to a location inside GITHUB_WORKSPACE. When GITHUB_WORKSPACE
// is not set (e.g. local dev / unit tests), the check is skipped — callers
// that write to RUNNER_TEMP still get protection from basename-only
// destinations.
export function assertPathWithinWorkspace(inputPath: string): string {
const workspace = process.env.GITHUB_WORKSPACE
if (!workspace) {
core.warning(
'GITHUB_WORKSPACE is not set; skipping manifest path containment check'
)
return inputPath
}
const resolvedWorkspace = fs.realpathSync(path.resolve(workspace))
// Resolve relative inputs against the workspace (not process.cwd()), so
// a relative `manifests:` input is interpreted consistently regardless of
// whether a prior step changed the working directory. Absolute paths are
// passed through unchanged and still validated below.
const absoluteInput = path.isAbsolute(inputPath)
? inputPath
: path.resolve(resolvedWorkspace, inputPath)
let resolvedInput: string
try {
resolvedInput = fs.realpathSync(absoluteInput)
} catch (e) {
throw new Error(
`manifest path ${inputPath} does not exist or is not readable: ${e}`
)
}
const rel = path.relative(resolvedWorkspace, resolvedInput)
if (
rel === '' ||
(rel !== '..' &&
!rel.startsWith('..' + path.sep) &&
!path.isAbsolute(rel))
) {
return resolvedInput
}
throw new Error(
`manifest path ${inputPath} resolves to ${resolvedInput}, ` +
`which is outside the workspace ${resolvedWorkspace}`
)
}
export function writeObjectsToFile(inputObjects: any[]): string[] {
const newFilePaths = []
@@ -110,20 +64,22 @@ export function writeManifestToFile(
}
export function moveFileToTmpDir(originalFilepath: string) {
const safeSource = assertPathWithinWorkspace(originalFilepath)
const tempDirectory = getTempDirectory()
const ext = path.extname(safeSource)
const base = path.basename(safeSource, ext)
const uniqueName = `${base}_${getCurrentTime()}_${moveCounter++}${ext}`
const newPath = path.join(tempDirectory, uniqueName)
const newPath = path.join(tempDirectory, originalFilepath)
core.debug(`reading original contents from path: ${originalFilepath}`)
const contents = fs.readFileSync(safeSource)
const contents = fs.readFileSync(originalFilepath).toString()
const dirName = path.dirname(newPath)
if (!fs.existsSync(dirName)) {
core.debug(`path ${dirName} doesn't exist yet, making new dir...`)
fs.mkdirSync(dirName, {recursive: true})
}
core.debug(`writing contents to new path ${newPath}`)
fs.writeFileSync(newPath, contents)
fs.writeFileSync(path.join(newPath), contents)
core.debug(`moved contents from ${originalFilepath} to ${newPath}`)
return newPath
}
@@ -153,20 +109,15 @@ export async function getFilesFromDirectoriesAndURLs(
`encountered error trying to pull YAML from URL ${fileName}: ${e}`
)
}
continue
}
const safePath = assertPathWithinWorkspace(fileName)
if (fs.lstatSync(safePath).isDirectory()) {
recurisveManifestGetter(safePath).forEach((file) => {
} else if (fs.lstatSync(fileName).isDirectory()) {
recurisveManifestGetter(fileName).forEach((file) => {
fullPathSet.add(file)
})
} else if (
getFileExtension(safePath) === 'yml' ||
getFileExtension(safePath) === 'yaml'
getFileExtension(fileName) === 'yml' ||
getFileExtension(fileName) === 'yaml'
) {
fullPathSet.add(safePath)
fullPathSet.add(fileName)
} else {
core.debug(
`Detected non-manifest file, ${fileName}, continuing... `
@@ -189,33 +140,24 @@ export async function writeYamlFromURLToFile(
): Promise<string> {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
.get(url, async (response) => {
const code = response.statusCode ?? 0
if (code >= 400) {
response.resume()
reject(
new Error(
Error(
`received response status ${response.statusMessage} from url ${url}`
)
)
return
}
const targetPath = getNewTempManifestFileName(
urlFileKind,
fileNumber.toString()
)
// Once the write stream is created the file exists on disk;
// route all post-stream rejections through this helper so we
// don't leave truncated YAML in RUNNER_TEMP for later tooling
// to pick up. Do NOT unlink on the success path.
const cleanupAndReject = (err: unknown) => {
fs.rm(targetPath, {force: true}, () => reject(err))
}
const fileWriter = fs.createWriteStream(targetPath)
fileWriter.on('error', cleanupAndReject)
fileWriter.on('finish', () => {
try {
// save the file to disk
const fileWriter = fs
.createWriteStream(targetPath)
.on('finish', () => {
const verification = verifyYaml(targetPath, url)
if (succeeded(verification)) {
core.debug(
@@ -225,16 +167,15 @@ export async function writeYamlFromURLToFile(
)
resolve(targetPath)
} else {
cleanupAndReject(new Error(verification.error))
reject(verification.error)
}
} catch (e) {
cleanupAndReject(e)
}
})
response.on('error', cleanupAndReject)
})
response.pipe(fileWriter)
})
.on('error', reject)
.on('error', (error) => {
reject(error)
})
})
}
@@ -258,7 +199,7 @@ function verifyYaml(filepath: string, url: string): Errorable<K8sObject[]> {
}
for (const obj of inputObjects) {
if (obj == null || !obj.kind || !obj.apiVersion || !obj.metadata) {
if (!obj.kind || !obj.apiVersion || !obj.metadata) {
return {
succeeded: false,
error: `failed to parse manifest from ${url}: missing fields`
@@ -280,7 +221,7 @@ function recurisveManifestGetter(dirName: string): string[] {
getFileExtension(fileName) === 'yml' ||
getFileExtension(fileName) === 'yaml'
) {
toRet.push(assertPathWithinWorkspace(fnwd))
toRet.push(path.join(dirName, fileName))
} else {
core.debug(`Detected non-manifest file, ${fileName}, continuing... `)
}
+1 -1
View File
@@ -2,7 +2,7 @@ import {
getNormalizedPath,
isHttpUrl,
normalizeWorkflowStrLabel
} from './githubUtils.js'
} from './githubUtils'
describe('Github utils', () => {
it('normalizes workflow string labels', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import {GitHubClient, OkStatusCode} from '../types/githubClient.js'
import {GitHubClient, OkStatusCode} from '../types/githubClient'
import * as core from '@actions/core'
export async function getWorkflowFilePath(
+2 -6
View File
@@ -1,9 +1,6 @@
import {vi} from 'vitest'
vi.mock('@actions/core')
import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import {checkForErrors} from './kubectlUtils.js'
import {checkForErrors} from './kubectlUtils'
describe('Kubectl utils', () => {
it('checks for errors', () => {
@@ -42,8 +39,7 @@ describe('Kubectl utils', () => {
).toThrow()
// with warn behavior
const warnSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
warnSpy.mockClear()
jest.spyOn(core, 'warning').mockImplementation(() => {})
let warningCalls = 0
expect(() => checkForErrors([success], true)).not.toThrow()
expect(core.warning).toHaveBeenCalledTimes(warningCalls)
+1 -1
View File
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import {Kubectl} from '../types/kubectl.js'
import {Kubectl} from '../types/kubectl'
const NAMESPACE = 'namespace'
+1 -1
View File
@@ -1,4 +1,4 @@
import {KubernetesWorkload} from '../types/kubernetesTypes.js'
import {KubernetesWorkload} from '../types/kubernetesTypes'
export function getImagePullSecrets(inputObject: any) {
const kind = inputObject?.kind?.toLowerCase()
+1 -1
View File
@@ -3,7 +3,7 @@ import {
isServiceEntity,
KubernetesWorkload,
NullInputObjectError
} from '../types/kubernetesTypes.js'
} from '../types/kubernetesTypes'
export function updateSpecLabels(
inputObject: any,
+45 -106
View File
@@ -1,29 +1,10 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
getInput: vi.fn().mockReturnValue(''),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn()
}
})
import * as manifestStabilityUtils from './manifestStabilityUtils.js'
import {Kubectl} from '../types/kubectl.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from '../actions/deploy.js'
import * as manifestStabilityUtils from './manifestStabilityUtils'
import {Kubectl} from '../types/kubectl'
import {ResourceTypeFleet, ResourceTypeManagedCluster} from '../actions/deploy'
import {ExecOutput} from '@actions/exec'
import {exitCode, stdout} from 'process'
import * as core from '@actions/core'
import * as timeUtils from './timeUtils.js'
import * as timeUtils from './timeUtils'
describe('manifestStabilityUtils', () => {
const kc = new Kubectl('')
@@ -36,8 +17,8 @@ describe('manifestStabilityUtils', () => {
]
it('should return immediately if the resource type is fleet', async () => {
const spy = vi.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = vi.spyOn(kc, 'checkRolloutStatus')
const spy = jest.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = jest.spyOn(kc, 'checkRolloutStatus')
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
@@ -49,8 +30,8 @@ describe('manifestStabilityUtils', () => {
})
it('should run fully if the resource type is managedCluster', async () => {
const spy = vi.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = vi
const spy = jest.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = jest
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
@@ -73,7 +54,7 @@ describe('manifestStabilityUtils', () => {
it('should pass timeout to checkRolloutStatus when provided', async () => {
const timeout = '300s'
const checkRolloutStatusSpy = vi
const checkRolloutStatusSpy = jest
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
@@ -101,7 +82,7 @@ describe('manifestStabilityUtils', () => {
})
it('should call checkRolloutStatus without timeout when not provided', async () => {
const checkRolloutStatusSpy = vi
const checkRolloutStatusSpy = jest
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
@@ -130,76 +111,34 @@ describe('manifestStabilityUtils', () => {
describe('checkManifestStability failure and resource-specific scenarios', () => {
let kc: Kubectl
let coreErrorSpy: MockInstance
let coreInfoSpy: MockInstance
let coreWarningSpy: MockInstance
let coreErrorSpy: jest.SpyInstance
let coreInfoSpy: jest.SpyInstance
let coreWarningSpy: jest.SpyInstance
beforeEach(() => {
kc = new Kubectl('', 'default')
coreErrorSpy = vi.spyOn(core, 'error').mockImplementation(() => {})
coreInfoSpy = vi.spyOn(core, 'info').mockImplementation(() => {})
coreWarningSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
kc = new Kubectl('')
coreErrorSpy = jest.spyOn(core, 'error').mockImplementation()
coreInfoSpy = jest.spyOn(core, 'info').mockImplementation()
coreWarningSpy = jest.spyOn(core, 'warning').mockImplementation()
})
afterEach(() => {
vi.restoreAllMocks()
jest.restoreAllMocks()
})
it('should call describe and collect errors when a rollout fails', async () => {
const resources = [
{type: 'deployment', name: 'failing-app', namespace: 'app-ns-123'}
{type: 'deployment', name: 'failing-app', namespace: 'default'}
]
const rolloutError = new Error('Progress deadline exceeded')
const describeOutput =
'Events:\n Type\tReason\tMessage\n Normal\tScalingReplicaSet\tScaled up replica set failing-app-123 to 1'
// Arrange: Mock rollout to fail and describe to succeed
const checkRolloutStatusSpy = vi
const checkRolloutStatusSpy = jest
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(rolloutError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: describeOutput,
stderr: '',
exitCode: 0
})
// Act & Assert: Expect the function to throw the final aggregated error
const expectedErrorMessage = `Rollout failed for deployment/failing-app in namespace app-ns-123: ${rolloutError.message}`
await expect(
manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
).rejects.toThrow(
`Rollout status failed for the following resources:\n${expectedErrorMessage}`
)
// Assert that the correct functions were called
expect(checkRolloutStatusSpy).toHaveBeenCalledTimes(1)
expect(coreErrorSpy).toHaveBeenCalledWith(expectedErrorMessage)
expect(describeSpy).toHaveBeenCalledWith(
'deployment',
'failing-app',
false,
'app-ns-123'
)
expect(coreInfoSpy).toHaveBeenCalledWith(
`Describe output for deployment/failing-app:\n${describeOutput}`
)
})
it('should use the default kubectl namespace when none is provided', async () => {
const resources = [{type: 'deployment', name: 'failing-app'}]
const rolloutError = new Error('Progress deadline exceeded')
const describeOutput =
'Events:\n Type\tReason\tMessage\n Normal\tScalingReplicaSet\tScaled up replica set failing-app-123 to 1'
// Arrange: Mock rollout to fail and describe to succeed
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(rolloutError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
const describeSpy = jest.spyOn(kc, 'describe').mockResolvedValue({
stdout: describeOutput,
stderr: '',
exitCode: 0
@@ -224,7 +163,7 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
'deployment',
'failing-app',
false,
undefined
'default'
)
expect(coreInfoSpy).toHaveBeenCalledWith(
`Describe output for deployment/failing-app:\n${describeOutput}`
@@ -235,10 +174,10 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
const resources = [{type: 'Pod', name: 'test-pod', namespace: 'default'}]
// Arrange: Spy on checkPodStatus and checkRolloutStatus
const checkPodStatusSpy = vi
const checkPodStatusSpy = jest
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockResolvedValue() // Assume pod becomes ready
const checkRolloutStatusSpy = vi.spyOn(kc, 'checkRolloutStatus')
const checkRolloutStatusSpy = jest.spyOn(kc, 'checkRolloutStatus')
// Act
await manifestStabilityUtils.checkManifestStability(
@@ -259,10 +198,10 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
const podError = new Error('Pod rollout failed')
// Arrange: Mock checkPodStatus to fail
const checkPodStatusSpy = vi
const checkPodStatusSpy = jest
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockRejectedValue(podError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
const describeSpy = jest.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output',
stderr: '',
exitCode: 0
@@ -290,7 +229,7 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
it('should wait for external IP for a LoadBalancer service', async () => {
//Spying on sleep to avoid actual delays in tests
vi.spyOn(timeUtils, 'sleep').mockResolvedValue(undefined)
jest.spyOn(timeUtils, 'sleep').mockResolvedValue(undefined)
const resources = [
{type: 'service', name: 'test-svc', namespace: 'default'}
]
@@ -304,7 +243,7 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
}
// Arrange: Mock getResource to simulate the IP being assigned on the second poll
const getResourceSpy = vi
const getResourceSpy = jest
.spyOn(kc, 'getResource')
// First call: Initial service check
.mockResolvedValueOnce({
@@ -347,10 +286,10 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
// Arrange: Mock getService to fail, and describe to succeed
// Note: We mock getResource because getService is a private helper
const getResourceSpy = vi
const getResourceSpy = jest
.spyOn(kc, 'getResource')
.mockRejectedValue(getServiceError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
const describeSpy = jest.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output',
stderr: '',
exitCode: 0
@@ -388,7 +327,7 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
}
// Arrange
const getResourceSpy = vi.spyOn(kc, 'getResource').mockResolvedValue({
const getResourceSpy = jest.spyOn(kc, 'getResource').mockResolvedValue({
stdout: JSON.stringify(clusterIpService),
stderr: '',
exitCode: 0
@@ -411,19 +350,19 @@ describe('checkManifestStability failure and resource-specific scenarios', () =>
describe('checkManifestStability additional scenarios', () => {
let kc: Kubectl
let coreErrorSpy: MockInstance
let coreInfoSpy: MockInstance
let coreWarningSpy: MockInstance
let coreErrorSpy: jest.SpyInstance
let coreInfoSpy: jest.SpyInstance
let coreWarningSpy: jest.SpyInstance
beforeEach(() => {
kc = new Kubectl('')
coreErrorSpy = vi.spyOn(core, 'error').mockImplementation(() => {})
coreInfoSpy = vi.spyOn(core, 'info').mockImplementation(() => {})
coreWarningSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
coreErrorSpy = jest.spyOn(core, 'error').mockImplementation()
coreInfoSpy = jest.spyOn(core, 'info').mockImplementation()
coreWarningSpy = jest.spyOn(core, 'warning').mockImplementation()
})
afterEach(() => {
vi.restoreAllMocks()
jest.restoreAllMocks()
})
it('should aggregate errors from deployment and pod failures', async () => {
@@ -435,15 +374,15 @@ describe('checkManifestStability additional scenarios', () => {
const podError = new Error('Pod not ready in time')
// Arrange: Mock failures
const checkRolloutStatusSpy = vi
const checkRolloutStatusSpy = jest
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(deploymentError)
// For pod: simulate a pod check failure
const checkPodStatusSpy = vi
const checkPodStatusSpy = jest
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockRejectedValue(podError)
// For both, simulate a successful describe call to provide additional details
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
const describeSpy = jest.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe aggregated output',
stderr: '',
exitCode: 0
@@ -482,25 +421,25 @@ describe('checkManifestStability additional scenarios', () => {
// Arrange:
// Deployment rollout succeeds
vi.spyOn(kc, 'checkRolloutStatus').mockResolvedValue({
jest.spyOn(kc, 'checkRolloutStatus').mockResolvedValue({
exitCode: 0,
stderr: '',
stdout: ''
})
// Pod becomes ready
vi.spyOn(manifestStabilityUtils, 'checkPodStatus').mockResolvedValue()
jest.spyOn(manifestStabilityUtils, 'checkPodStatus').mockResolvedValue()
// Simulate a LoadBalancer service that already has an external IP
const stableService = {
spec: {type: 'LoadBalancer'},
status: {loadBalancer: {ingress: [{ip: '1.2.3.4'}]}}
}
vi.spyOn(kc, 'getResource').mockResolvedValue({
jest.spyOn(kc, 'getResource').mockResolvedValue({
stdout: JSON.stringify(stableService),
stderr: '',
exitCode: 0
})
// Provide a describe result to avoid warnings
vi.spyOn(kc, 'describe').mockResolvedValue({
jest.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output stable',
stderr: '',
exitCode: 0
+8 -8
View File
@@ -1,10 +1,10 @@
import * as core from '@actions/core'
import * as KubernetesConstants from '../types/kubernetesTypes.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {checkForErrors} from './kubectlUtils.js'
import {sleep} from './timeUtils.js'
import {ResourceTypeFleet} from '../actions/deploy.js'
import {ClusterType} from '../inputUtils.js'
import * as KubernetesConstants from '../types/kubernetesTypes'
import {Kubectl, Resource} from '../types/kubectl'
import {checkForErrors} from './kubectlUtils'
import {sleep} from './timeUtils'
import {ResourceTypeFleet} from '../actions/deploy'
import {ClusterType} from '../inputUtils'
const IS_SILENT = false
const POD = 'pod'
@@ -46,7 +46,7 @@ export async function checkManifestStability(
)
checkForErrors([result])
} catch (ex) {
const errorMessage = `Rollout failed for ${resource.type}/${resource.name} in namespace ${kubectl.getNamespace(resource.namespace)}: ${ex.message || ex}`
const errorMessage = `Rollout failed for ${resource.type}/${resource.name} in namespace ${resource.namespace}: ${ex.message || ex}`
core.error(errorMessage)
rolloutErrors.push(errorMessage)
@@ -106,7 +106,7 @@ export async function checkManifestStability(
}
}
} catch (ex) {
const errorMessage = `Could not determine service status of: ${resource.name} in namespace ${kubectl.getNamespace(resource.namespace)}. Error: ${ex.message || ex}`
const errorMessage = `Could not determine service status of: ${resource.name} in namespace ${resource.namespace}. Error: ${ex.message || ex}`
core.warning(errorMessage)
try {
+5 -8
View File
@@ -1,17 +1,14 @@
import {vi} from 'vitest'
vi.mock('fs')
import * as fileUtils from './fileUtils.js'
import * as manifestUpdateUtils from './manifestUpdateUtils.js'
import * as fileUtils from './fileUtils'
import * as manifestUpdateUtils from './manifestUpdateUtils'
import * as path from 'path'
import * as fs from 'fs'
describe('manifestUpdateUtils', () => {
vi.spyOn(fileUtils, 'moveFileToTmpDir').mockImplementation((filename) => {
jest.spyOn(fileUtils, 'moveFileToTmpDir').mockImplementation((filename) => {
return path.join('/tmp', filename)
})
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})
+7 -7
View File
@@ -2,25 +2,25 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as path from 'path'
import * as fileHelper from './fileUtils.js'
import {moveFileToTmpDir} from './fileUtils.js'
import * as fileHelper from './fileUtils'
import {moveFileToTmpDir} from './fileUtils'
import {
InputObjectKindNotDefinedError,
InputObjectMetadataNotDefinedError,
isWorkloadEntity,
KubernetesWorkload,
NullInputObjectError
} from '../types/kubernetesTypes.js'
} from '../types/kubernetesTypes'
import {
getSpecSelectorLabels,
setSpecSelectorLabels
} from './manifestSpecLabelUtils.js'
} from './manifestSpecLabelUtils'
import {
getImagePullSecrets,
setImagePullSecrets
} from './manifestPullSecretUtils.js'
import {Resource} from '../types/kubectl.js'
import {K8sObject} from '../types/k8sObject.js'
} from './manifestPullSecretUtils'
import {Resource} from '../types/kubectl'
import {K8sObject} from '../types/k8sObject'
export function updateManifestFiles(manifestFilePaths: string[]) {
if (manifestFilePaths?.length === 0) {
+5 -4
View File
@@ -1,12 +1,11 @@
import {
getImagePullSecrets,
setImagePullSecrets
} from './manifestPullSecretUtils.js'
import {updateSpecLabels} from './manifestSpecLabelUtils.js'
import {getReplicaCount} from './manifestUpdateUtils.js'
} from './manifestPullSecretUtils'
import {updateSpecLabels} from './manifestSpecLabelUtils'
import {getReplicaCount} from './manifestUpdateUtils'
import * as yaml from 'js-yaml'
import * as fs from 'fs'
import {isWorkloadEntity, isDeploymentEntity} from '../types/kubernetesTypes.js'
describe('ScaledJob Support', () => {
let scaledJobObject: any
@@ -58,11 +57,13 @@ describe('ScaledJob Support', () => {
describe('Workload Classification', () => {
it('should classify ScaledJob as workload entity', () => {
const {isWorkloadEntity} = require('../types/kubernetesTypes')
expect(isWorkloadEntity('ScaledJob')).toBe(true)
expect(isWorkloadEntity('scaledjob')).toBe(true)
})
it('should not classify ScaledJob as deployment entity', () => {
const {isDeploymentEntity} = require('../types/kubernetesTypes')
expect(isDeploymentEntity('scaledjob')).toBe(false)
expect(isDeploymentEntity('ScaledJob')).toBe(false)
})
+1 -1
View File
@@ -1,4 +1,4 @@
import {Kubectl} from '../types/kubectl.js'
import {Kubectl} from '../types/kubectl'
const trafficSplitAPIVersionPrefix = 'split.smi-spec.io'
@@ -2,7 +2,7 @@ import {
cleanLabel,
removeInvalidLabelCharacters,
VALID_LABEL_REGEX
} from '../utilities/workflowAnnotationUtils.js'
} from '../utilities/workflowAnnotationUtils'
describe('WorkflowAnnotationUtils', () => {
describe('cleanLabel', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import {DeploymentConfig} from '../types/deploymentConfig.js'
import {DeploymentConfig} from '../types/deploymentConfig'
const ANNOTATION_PREFIX = 'actions.github.com'
+4 -11
View File
@@ -1,15 +1,8 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["vitest/globals"],
"esModuleInterop": true,
"strict": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": false,
"sourceMap": true
"target": "ES6",
"module": "commonjs",
"esModuleInterop": true
},
"exclude": ["node_modules", "test", "lib"]
"exclude": ["node_modules", "test", "src/**/*.test.ts"]
}
-11
View File
@@ -1,11 +0,0 @@
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
testTimeout: 9000,
clearMocks: true
}
})