Compare commits

..

1 Commits

Author SHA1 Message Date
GitHub Action c8cfec839d build 2026-03-03 21:17:31 +00:00
23 changed files with 30846 additions and 1296 deletions
+4 -4
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.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@c793b717bc78562f491db7b0e93a3a178b099162 #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@c793b717bc78562f491db7b0e93a3a178b099162 #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@c793b717bc78562f491db7b0e93a3a178b099162 #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@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.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@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -21,7 +21,7 @@ jobs:
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
@@ -19,7 +19,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
@@ -30,13 +30,13 @@ jobs:
run: npm run build
- 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
@@ -18,7 +18,7 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
+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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: |
npm install
npm test
-1
View File
@@ -2,6 +2,5 @@ node_modules
.DS_Store
.idea
lib/
coverage/
-17
View File
@@ -1,22 +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
+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'
+30274
View File
File diff suppressed because one or more lines are too long
+492 -789
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "k8s-deploy-action",
"version": "6.0.0",
"version": "5.1.0",
"author": "Deepak Sattiraju",
"license": "MIT",
"type": "module",
@@ -14,23 +14,23 @@
"prepare": "husky"
},
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/core": "^3.0.0",
"@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",
"js-yaml": "4.1.1",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^25.9.3",
"esbuild": "^0.28",
"@types/node": "^25.3.3",
"esbuild": "^0.27",
"husky": "^9.1.7",
"prettier": "^3.8.4",
"typescript": "6.0.3",
"prettier": "^3.8.1",
"typescript": "5.9.3",
"vitest": "^4"
}
}
+11 -361
View File
@@ -3,16 +3,12 @@ import * as fileUtils from './fileUtils.js'
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'
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 +53,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()))
@@ -109,366 +105,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', () => {
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.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([])
})
})
+28 -87
View File
@@ -11,56 +11,10 @@ import {K8sObject} from '../types/k8sObject.js'
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... `)
}