mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-16 17:56:44 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da5474a5d | ||
|
|
375992cd31 | ||
|
|
aae800a69b | ||
|
|
e47f9b8af4 | ||
|
|
54e14cb7f3 | ||
|
|
ef2229fc0b | ||
|
|
88dc98f8ef | ||
|
|
b388518d40 | ||
|
|
7afb8f9323 | ||
|
|
d4c5425b22 | ||
|
|
120636d3d7 | ||
|
|
5e805a0546 | ||
|
|
27bae0b2b7 | ||
|
|
8eed1ad1b6 | ||
|
|
7b404841b2 | ||
|
|
977d53963d | ||
|
|
77b40ac6df | ||
|
|
ee10d95fd4 | ||
|
|
73655d4639 | ||
|
|
ca4ea17d58 | ||
|
|
ed70e2f8e0 | ||
|
|
aeabaf144a | ||
|
|
8388a36f44 | ||
|
|
9705deeb08 | ||
|
|
99efdeca99 | ||
|
|
bb09a79b22 | ||
|
|
746e644039 | ||
|
|
7223e1dbb2 | ||
|
|
af27abe1f7 | ||
|
|
638bd19c9d | ||
|
|
50e14cf868 | ||
|
|
921be5b85f | ||
|
|
0cce49705b |
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: sed -i "s|{{PATHTOREPO}}|$(pwd)|" packages/k8s/tests/test-kind.yaml
|
||||
name: Setup kind cluster yaml config
|
||||
- uses: helm/kind-action@v1.2.0
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,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@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -69,4 +69,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
88
.github/workflows/release.yaml
vendored
88
.github/workflows/release.yaml
vendored
@@ -1,76 +1,70 @@
|
||||
name: CD - Release new version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm install
|
||||
name: Install dependencies
|
||||
- run: npm run bootstrap
|
||||
name: Bootstrap the packages
|
||||
- run: npm run build-all
|
||||
name: Build packages
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Bootstrap the packages
|
||||
run: npm run bootstrap
|
||||
|
||||
- name: Build packages
|
||||
run: npm run build-all
|
||||
|
||||
- uses: actions/github-script@v7
|
||||
id: releaseVersion
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const hookVersion = require('./package.json').version
|
||||
core.setOutput('version', hookVersion);
|
||||
return require('./package.json').version
|
||||
|
||||
- name: Zip up releases
|
||||
run: |
|
||||
zip -r -j actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.version }}.zip packages/docker/dist
|
||||
zip -r -j actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.version }}.zip packages/k8s/dist
|
||||
zip -r -j actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.result }}.zip packages/docker/dist
|
||||
zip -r -j actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.result }}.zip packages/k8s/dist
|
||||
|
||||
- name: Calculate SHA
|
||||
id: sha
|
||||
shell: bash
|
||||
run: |
|
||||
sha_docker=$(sha256sum actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.version }}.zip | awk '{print $1}')
|
||||
sha_docker=$(sha256sum actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.result }}.zip | awk '{print $1}')
|
||||
echo "Docker SHA: $sha_docker"
|
||||
echo "docker-sha=$sha_docker" >> $GITHUB_OUTPUT
|
||||
sha_k8s=$(sha256sum actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.version }}.zip | awk '{print $1}')
|
||||
sha_k8s=$(sha256sum actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.result }}.zip | awk '{print $1}')
|
||||
echo "K8s SHA: $sha_k8s"
|
||||
echo "k8s-sha=$sha_k8s" >> $GITHUB_OUTPUT
|
||||
- name: replace SHA
|
||||
|
||||
- name: Create release notes
|
||||
id: releaseNotes
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
var releaseNotes = fs.readFileSync('${{ github.workspace }}/releaseNotes.md', 'utf8').replace(/<HOOK_VERSION>/g, '${{ steps.releaseVersion.outputs.version }}')
|
||||
var releaseNotes = fs.readFileSync('${{ github.workspace }}/releaseNotes.md', 'utf8').replace(/<HOOK_VERSION>/g, '${{ steps.releaseVersion.outputs.result }}')
|
||||
releaseNotes = releaseNotes.replace(/<DOCKER_SHA>/g, '${{ steps.sha.outputs.docker-sha }}')
|
||||
releaseNotes = releaseNotes.replace(/<K8S_SHA>/g, '${{ steps.sha.outputs.k8s-sha }}')
|
||||
console.log(releaseNotes)
|
||||
core.setOutput('note', releaseNotes);
|
||||
- uses: actions/create-release@v1
|
||||
id: createRelease
|
||||
name: Create ${{ steps.releaseVersion.outputs.version }} Hook Release
|
||||
fs.writeFileSync('${{ github.workspace }}/finalReleaseNotes.md', releaseNotes);
|
||||
|
||||
- name: Create ${{ steps.releaseVersion.outputs.result }} Hook Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: "v${{ steps.releaseVersion.outputs.version }}"
|
||||
release_name: "v${{ steps.releaseVersion.outputs.version }}"
|
||||
body: |
|
||||
${{ steps.releaseNotes.outputs.note }}
|
||||
- name: Upload K8s hooks
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ${{ github.workspace }}/actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.version }}.zip
|
||||
asset_name: actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.version }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload docker hooks
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ${{ github.workspace }}/actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.version }}.zip
|
||||
asset_name: actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.version }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create v${{ steps.releaseVersion.outputs.result }} \
|
||||
--title "v${{ steps.releaseVersion.outputs.result }}" \
|
||||
--repo ${{ github.repository }} \
|
||||
--notes-file ${{ github.workspace }}/finalReleaseNotes.md \
|
||||
--latest \
|
||||
${{ github.workspace }}/actions-runner-hooks-k8s-${{ steps.releaseVersion.outputs.result }}.zip \
|
||||
${{ github.workspace }}/actions-runner-hooks-docker-${{ steps.releaseVersion.outputs.result }}.zip
|
||||
@@ -1 +1 @@
|
||||
* @actions/actions-launch @actions/runner-akvelon
|
||||
* @actions/actions-launch
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date:** 3 August 2023
|
||||
|
||||
**Status**: Accepted <!--Accepted|Rejected|Superceded|Deprecated-->
|
||||
**Status**: Superceded [^1]
|
||||
|
||||
## Context
|
||||
|
||||
@@ -30,3 +30,5 @@ In case the hook is able to read the extended spec, it will first create a defau
|
||||
## Consequences
|
||||
|
||||
The addition of hook extensions will provide a better user experience for users who need to customize the pods created by the container hook. However, it will require additional effort to provide the template to the runner pod, and configure it properly.
|
||||
|
||||
[^1]: Superseded by [ADR 0134](0134-hook-extensions.md)
|
||||
41
docs/adrs/0134-hook-extensions.md
Normal file
41
docs/adrs/0134-hook-extensions.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# ADR 0134: Hook extensions
|
||||
|
||||
**Date:** 20 February 2024
|
||||
|
||||
**Status**: Accepted [^1]
|
||||
|
||||
## Context
|
||||
|
||||
The current implementation of container hooks does not allow users to customize the pods created by the hook.
|
||||
While the implementation is designed to be used as is or as a starting point, building and maintaining a custom hook implementation just to specify additional fields is not a good user experience.
|
||||
|
||||
## Decision
|
||||
|
||||
We have decided to add hook extensions to the container hook implementation.
|
||||
This will allow users to customize the pods created by the hook by specifying additional fields.
|
||||
The hook extensions will be implemented in a way that is backwards-compatible with the existing hook implementation.
|
||||
|
||||
To allow customization, the runner executing the hook should have `ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE` environment variable pointing to a yaml file on the runner system.
|
||||
The extension specified in that file will be applied both for job pods, and container steps.
|
||||
|
||||
If environment variable is set, but the file can't be read, the hook will fail, signaling incorrect configuration.
|
||||
|
||||
If the environment variable does not exist, the hook will apply the default spec.
|
||||
|
||||
In case the hook is able to read the extended spec, it will first create a default configuration, and then merged modified fields in the following way:
|
||||
|
||||
1. The `.metadata` fields that will be appended if they are not reserved are `labels` and `annotations`.
|
||||
2. The pod spec fields except for `containers` and `volumes` are applied from the template, possibly overwriting the field.
|
||||
3. The volumes are applied in form of appending additional volumes to the default volumes.
|
||||
4. The containers are merged based on the name assigned to them:
|
||||
1. If the name of the container *is* "$job", the `name` and the `image` fields are going to be ignored and the spec will be applied so that `env`, `volumeMounts`, `ports` are appended to the default container spec created by the hook, while the rest of the fields are going to be applied to the newly created container spec.
|
||||
2. If the name of the container *starts with* "$", and matches the name of the [container service](https://docs.github.com/en/actions/using-containerized-services/about-service-containers), the `name` and the `image` fields are going to be ignored and the spec will be applied to that service container, so that `env`, `volumeMounts`, `ports` are appended to the default container spec for service created by the hook, while the rest of the fields are going to be applied to the created container spec.
|
||||
If there is no container service with such name defined in the workflow, such spec extension will be ignored.
|
||||
3. If the name of the container *does not start with* "$", the entire spec of the container will be added to the pod definition.
|
||||
|
||||
## Consequences
|
||||
|
||||
The addition of hook extensions will provide a better user experience for users who need to customize the pods created by the container hook.
|
||||
However, it will require additional effort to provide the template to the runner pod, and configure it properly.
|
||||
|
||||
[^1]: Supersedes [ADR 0096](0096-hook-extensions.md)
|
||||
@@ -9,7 +9,7 @@ spec:
|
||||
runAsGroup: 3000
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: $job # overwirtes job container
|
||||
- name: $job # overwrites job container
|
||||
env:
|
||||
- name: ENV1
|
||||
value: "value1"
|
||||
@@ -20,11 +20,22 @@ spec:
|
||||
args:
|
||||
- -c
|
||||
- sleep 50
|
||||
- name: $redis # overwrites redis service
|
||||
env:
|
||||
- name: ENV2
|
||||
value: "value2"
|
||||
image: "busybox:1.28" # Ignored
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Mi"
|
||||
cpu: "1"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "2"
|
||||
- name: side-car
|
||||
image: "ubuntu:latest" # required
|
||||
command:
|
||||
- sh
|
||||
- sh
|
||||
args:
|
||||
- -c
|
||||
- sleep 60
|
||||
|
||||
- -c
|
||||
- sleep 60
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"state": {},
|
||||
"args": {
|
||||
"container": {
|
||||
"image": "node:14.16",
|
||||
"image": "node:22",
|
||||
"workingDirectory": "/__w/repo/repo",
|
||||
"createOptions": "--cpus 1",
|
||||
"environmentVariables": {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
},
|
||||
"args": {
|
||||
"image": "node:14.16",
|
||||
"image": "node:22",
|
||||
"dockerfile": null,
|
||||
"entryPointArgs": [
|
||||
"-e",
|
||||
|
||||
60
package-lock.json
generated
60
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hooks",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hooks",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.1",
|
||||
@@ -457,12 +457,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1219,9 +1219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -1889,12 +1889,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2322,9 +2322,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -2942,12 +2942,12 @@
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -3513,9 +3513,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4000,12 +4000,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
@@ -4292,9 +4292,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hooks",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Three projects are included - k8s: a kubernetes hook implementation that spins up pods dynamically to run a job - docker: A hook implementation of the runner's docker implementation - A hook lib, which contains shared typescript definitions and utilities that the other packages consume",
|
||||
"main": "",
|
||||
"directories": {
|
||||
|
||||
40
packages/docker/package-lock.json
generated
40
packages/docker/package-lock.json
generated
@@ -1807,12 +1807,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2638,9 +2638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -5326,9 +5326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
@@ -6790,12 +6790,12 @@
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"browser-process-hrtime": {
|
||||
@@ -7424,9 +7424,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -9452,9 +9452,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
|
||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "jest --runInBand",
|
||||
"build": "npx tsc && npx ncc build"
|
||||
"build": "npx tsc && npx ncc build",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -43,11 +43,16 @@ export async function createContainer(
|
||||
|
||||
if (args.environmentVariables) {
|
||||
for (const [key] of Object.entries(args.environmentVariables)) {
|
||||
dockerArgs.push('-e')
|
||||
dockerArgs.push(key)
|
||||
dockerArgs.push('-e', key)
|
||||
}
|
||||
}
|
||||
|
||||
dockerArgs.push('-e', 'GITHUB_ACTIONS=true')
|
||||
// Use same behavior as the runner https://github.com/actions/runner/blob/27d9c886ab9a45e0013cb462529ac85d581f8c41/src/Runner.Worker/Container/DockerCommandManager.cs#L150
|
||||
if (!('CI' in (args.environmentVariables ?? {}))) {
|
||||
dockerArgs.push('-e', 'CI=true')
|
||||
}
|
||||
|
||||
const mountVolumes = [
|
||||
...(args.userMountVolumes || []),
|
||||
...(args.systemMountVolumes || [])
|
||||
@@ -403,11 +408,16 @@ export async function containerRun(
|
||||
}
|
||||
if (args.environmentVariables) {
|
||||
for (const [key] of Object.entries(args.environmentVariables)) {
|
||||
dockerArgs.push('-e')
|
||||
dockerArgs.push(key)
|
||||
dockerArgs.push('-e', key)
|
||||
}
|
||||
}
|
||||
|
||||
dockerArgs.push('-e', 'GITHUB_ACTIONS=true')
|
||||
// Use same behavior as the runner https://github.com/actions/runner/blob/27d9c886ab9a45e0013cb462529ac85d581f8c41/src/Runner.Worker/Container/DockerCommandManager.cs#L150
|
||||
if (!('CI' in (args.environmentVariables ?? {}))) {
|
||||
dockerArgs.push('-e', 'CI=true')
|
||||
}
|
||||
|
||||
const mountVolumes = [
|
||||
...(args.userMountVolumes || []),
|
||||
...(args.systemMountVolumes || [])
|
||||
|
||||
@@ -31,9 +31,13 @@ export async function prepareJob(
|
||||
core.info('No containers exist, skipping hook invocation')
|
||||
exit(0)
|
||||
}
|
||||
const networkName = generateNetworkName()
|
||||
// Create network
|
||||
await networkCreate(networkName)
|
||||
|
||||
let networkName = process.env.ACTIONS_RUNNER_NETWORK_DRIVER
|
||||
if (!networkName) {
|
||||
networkName = generateNetworkName()
|
||||
// Create network
|
||||
await networkCreate(networkName)
|
||||
}
|
||||
|
||||
// Create Job Container
|
||||
let containerMetadata: ContainerMetadata | undefined = undefined
|
||||
|
||||
@@ -75,4 +75,22 @@ describe('run script step', () => {
|
||||
runScriptStep(definitions.runScriptStep.args, prepareJobResponse.state)
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Should confirm that CI and GITHUB_ACTIONS are set', async () => {
|
||||
definitions.runScriptStep.args.entryPoint = '/bin/bash'
|
||||
definitions.runScriptStep.args.entryPointArgs = [
|
||||
'-c',
|
||||
`'if [[ ! $(env | grep "^CI=") = "CI=true" ]]; then exit 1; fi'`
|
||||
]
|
||||
await expect(
|
||||
runScriptStep(definitions.runScriptStep.args, prepareJobResponse.state)
|
||||
).resolves.not.toThrow()
|
||||
definitions.runScriptStep.args.entryPointArgs = [
|
||||
'-c',
|
||||
`'if [[ ! $(env | grep "^GITHUB_ACTIONS=") = "GITHUB_ACTIONS=true" ]]; then exit 1; fi'`
|
||||
]
|
||||
await expect(
|
||||
runScriptStep(definitions.runScriptStep.args, prepareJobResponse.state)
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
28
packages/hooklib/package-lock.json
generated
28
packages/hooklib/package-lock.json
generated
@@ -477,12 +477,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1227,9 +1227,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -2862,12 +2862,12 @@
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -3424,9 +3424,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
||||
1115
packages/k8s/package-lock.json
generated
1115
packages/k8s/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@
|
||||
"@actions/core": "^1.9.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/io": "^1.1.2",
|
||||
"@kubernetes/client-node": "^0.18.1",
|
||||
"@kubernetes/client-node": "^0.22.2",
|
||||
"hooklib": "file:../hooklib",
|
||||
"js-yaml": "^4.1.0",
|
||||
"shlex": "^2.1.2"
|
||||
|
||||
@@ -41,6 +41,7 @@ export function getSecretName(): string {
|
||||
|
||||
export const MAX_POD_NAME_LENGTH = 63
|
||||
export const STEP_POD_NAME_SUFFIX_LENGTH = 8
|
||||
export const CONTAINER_EXTENSION_PREFIX = '$'
|
||||
export const JOB_CONTAINER_NAME = 'job'
|
||||
export const JOB_CONTAINER_EXTENSION_NAME = '$job'
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
PodPhase,
|
||||
fixArgs
|
||||
} from '../k8s/utils'
|
||||
import { JOB_CONTAINER_EXTENSION_NAME, JOB_CONTAINER_NAME } from './constants'
|
||||
import { CONTAINER_EXTENSION_PREFIX, JOB_CONTAINER_NAME } from './constants'
|
||||
|
||||
export async function prepareJob(
|
||||
args: PrepareJobArgs,
|
||||
@@ -60,7 +60,7 @@ export async function prepareJob(
|
||||
service,
|
||||
generateContainerName(service.image),
|
||||
false,
|
||||
undefined
|
||||
extension
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -79,7 +79,9 @@ export async function prepareJob(
|
||||
)
|
||||
} catch (err) {
|
||||
await prunePods()
|
||||
throw new Error(`failed to create job pod: ${err}`)
|
||||
core.debug(`createPod failed: ${JSON.stringify(err)}`)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to create job pod: ${message}`)
|
||||
}
|
||||
|
||||
if (!createdPod?.metadata?.name) {
|
||||
@@ -98,7 +100,7 @@ export async function prepareJob(
|
||||
)
|
||||
} catch (err) {
|
||||
await prunePods()
|
||||
throw new Error(`Pod failed to come online with error: ${err}`)
|
||||
throw new Error(`pod failed to come online with error: ${err}`)
|
||||
}
|
||||
|
||||
core.debug('Job pod is ready for traffic')
|
||||
@@ -110,14 +112,19 @@ export async function prepareJob(
|
||||
JOB_CONTAINER_NAME
|
||||
)
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to determine if the pod is alpine: ${err}`)
|
||||
core.debug(
|
||||
`Failed to determine if the pod is alpine: ${JSON.stringify(err)}`
|
||||
)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to determine if the pod is alpine: ${message}`)
|
||||
}
|
||||
core.debug(`Setting isAlpine to ${isAlpine}`)
|
||||
generateResponseFile(responseFile, createdPod, isAlpine)
|
||||
generateResponseFile(responseFile, args, createdPod, isAlpine)
|
||||
}
|
||||
|
||||
function generateResponseFile(
|
||||
responseFile: string,
|
||||
args: PrepareJobArgs,
|
||||
appPod: k8s.V1Pod,
|
||||
isAlpine
|
||||
): void {
|
||||
@@ -150,24 +157,27 @@ function generateResponseFile(
|
||||
}
|
||||
}
|
||||
|
||||
const serviceContainers = appPod.spec?.containers.filter(
|
||||
c => c.name !== JOB_CONTAINER_NAME
|
||||
)
|
||||
if (serviceContainers?.length) {
|
||||
response.context['services'] = serviceContainers.map(c => {
|
||||
const ctxPorts: ContextPorts = {}
|
||||
if (c.ports?.length) {
|
||||
for (const port of c.ports) {
|
||||
ctxPorts[port.containerPort] = port.hostPort
|
||||
}
|
||||
}
|
||||
if (args.services?.length) {
|
||||
const serviceContainerNames =
|
||||
args.services?.map(s => generateContainerName(s.image)) || []
|
||||
|
||||
return {
|
||||
image: c.image,
|
||||
ports: ctxPorts
|
||||
}
|
||||
})
|
||||
response.context['services'] = appPod?.spec?.containers
|
||||
?.filter(c => serviceContainerNames.includes(c.name))
|
||||
.map(c => {
|
||||
const ctxPorts: ContextPorts = {}
|
||||
if (c.ports?.length) {
|
||||
for (const port of c.ports) {
|
||||
ctxPorts[port.containerPort] = port.hostPort
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
image: c.image,
|
||||
ports: ctxPorts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
writeToResponseFile(responseFile, JSON.stringify(response))
|
||||
}
|
||||
|
||||
@@ -219,6 +229,18 @@ export function createContainerSpec(
|
||||
}
|
||||
}
|
||||
|
||||
podContainer.env.push({
|
||||
name: 'GITHUB_ACTIONS',
|
||||
value: 'true'
|
||||
})
|
||||
|
||||
if (!('CI' in container['environmentVariables'])) {
|
||||
podContainer.env.push({
|
||||
name: 'CI',
|
||||
value: 'true'
|
||||
})
|
||||
}
|
||||
|
||||
podContainer.volumeMounts = containerVolumes(
|
||||
container.userMountVolumes,
|
||||
jobContainer
|
||||
@@ -229,7 +251,7 @@ export function createContainerSpec(
|
||||
}
|
||||
|
||||
const from = extension.spec?.containers?.find(
|
||||
c => c.name === JOB_CONTAINER_EXTENSION_NAME
|
||||
c => c.name === CONTAINER_EXTENSION_PREFIX + name
|
||||
)
|
||||
|
||||
if (from) {
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from '../k8s'
|
||||
import {
|
||||
containerVolumes,
|
||||
PodPhase,
|
||||
fixArgs,
|
||||
mergeContainerWithOptions,
|
||||
readExtensionFromFile,
|
||||
fixArgs
|
||||
PodPhase,
|
||||
readExtensionFromFile
|
||||
} from '../k8s/utils'
|
||||
import { JOB_CONTAINER_EXTENSION_NAME, JOB_CONTAINER_NAME } from './constants'
|
||||
|
||||
@@ -28,7 +28,20 @@ export async function runContainerStep(
|
||||
|
||||
let secretName: string | undefined = undefined
|
||||
if (stepContainer.environmentVariables) {
|
||||
secretName = await createSecretForEnvs(stepContainer.environmentVariables)
|
||||
try {
|
||||
const envs = JSON.parse(
|
||||
JSON.stringify(stepContainer.environmentVariables)
|
||||
)
|
||||
envs['GITHUB_ACTIONS'] = 'true'
|
||||
if (!('CI' in envs)) {
|
||||
envs.CI = 'true'
|
||||
}
|
||||
secretName = await createSecretForEnvs(envs)
|
||||
} catch (err) {
|
||||
core.debug(`createSecretForEnvs failed: ${JSON.stringify(err)}`)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to create script environment: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const extension = readExtensionFromFile()
|
||||
@@ -36,7 +49,15 @@ export async function runContainerStep(
|
||||
core.debug(`Created secret ${secretName} for container job envs`)
|
||||
const container = createContainerSpec(stepContainer, secretName, extension)
|
||||
|
||||
const job = await createJob(container, extension)
|
||||
let job: k8s.V1Job
|
||||
try {
|
||||
job = await createJob(container, extension)
|
||||
} catch (err) {
|
||||
core.debug(`createJob failed: ${JSON.stringify(err)}`)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to run script step: ${message}`)
|
||||
}
|
||||
|
||||
if (!job.metadata?.name) {
|
||||
throw new Error(
|
||||
`Expected job ${JSON.stringify(
|
||||
@@ -46,10 +67,23 @@ export async function runContainerStep(
|
||||
}
|
||||
core.debug(`Job created, waiting for pod to start: ${job.metadata?.name}`)
|
||||
|
||||
const podName = await getContainerJobPodName(job.metadata.name)
|
||||
let podName: string
|
||||
try {
|
||||
podName = await getContainerJobPodName(job.metadata.name)
|
||||
} catch (err) {
|
||||
core.debug(`getContainerJobPodName failed: ${JSON.stringify(err)}`)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to get container job pod name: ${message}`)
|
||||
}
|
||||
|
||||
await waitForPodPhases(
|
||||
podName,
|
||||
new Set([PodPhase.COMPLETED, PodPhase.RUNNING, PodPhase.SUCCEEDED]),
|
||||
new Set([
|
||||
PodPhase.COMPLETED,
|
||||
PodPhase.RUNNING,
|
||||
PodPhase.SUCCEEDED,
|
||||
PodPhase.FAILED
|
||||
]),
|
||||
new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
|
||||
)
|
||||
core.debug('Container step is running or complete, pulling logs')
|
||||
@@ -58,6 +92,7 @@ export async function runContainerStep(
|
||||
|
||||
core.debug('Waiting for container job to complete')
|
||||
await waitForJobToComplete(job.metadata.name)
|
||||
|
||||
// pod has failed so pull the status code from the container
|
||||
const status = await getPodStatus(podName)
|
||||
if (status?.phase === 'Succeeded') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import * as fs from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import { RunScriptStepArgs } from 'hooklib'
|
||||
import { execPodStep } from '../k8s'
|
||||
import { writeEntryPointScript } from '../k8s/utils'
|
||||
@@ -28,7 +29,9 @@ export async function runScriptStep(
|
||||
JOB_CONTAINER_NAME
|
||||
)
|
||||
} catch (err) {
|
||||
throw new Error(`failed to run script step: ${err}`)
|
||||
core.debug(`execPodStep failed: ${JSON.stringify(err)}`)
|
||||
const message = (err as any)?.response?.body?.message || err
|
||||
throw new Error(`failed to run script step: ${message}`)
|
||||
} finally {
|
||||
fs.rmSync(runnerPath)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
PodPhase,
|
||||
mergePodSpecWithOptions,
|
||||
mergeObjectMeta,
|
||||
useKubeScheduler
|
||||
useKubeScheduler,
|
||||
fixArgs
|
||||
} from './utils'
|
||||
|
||||
const kc = new k8s.KubeConfig()
|
||||
@@ -94,10 +95,12 @@ export async function createPod(
|
||||
appPod.spec.containers = containers
|
||||
appPod.spec.restartPolicy = 'Never'
|
||||
|
||||
if (!useKubeScheduler()) {
|
||||
appPod.spec.nodeName = await getCurrentNodeName()
|
||||
const nodeName = await getCurrentNodeName()
|
||||
if (useKubeScheduler()) {
|
||||
appPod.spec.affinity = await getPodAffinity(nodeName)
|
||||
} else {
|
||||
appPod.spec.nodeName = nodeName
|
||||
}
|
||||
|
||||
const claimName = getVolumeClaimName()
|
||||
appPod.spec.volumes = [
|
||||
{
|
||||
@@ -154,8 +157,11 @@ export async function createJob(
|
||||
job.spec.template.spec.containers = [container]
|
||||
job.spec.template.spec.restartPolicy = 'Never'
|
||||
|
||||
if (!useKubeScheduler()) {
|
||||
job.spec.template.spec.nodeName = await getCurrentNodeName()
|
||||
const nodeName = await getCurrentNodeName()
|
||||
if (useKubeScheduler()) {
|
||||
job.spec.template.spec.affinity = await getPodAffinity(nodeName)
|
||||
} else {
|
||||
job.spec.template.spec.nodeName = nodeName
|
||||
}
|
||||
|
||||
const claimName = getVolumeClaimName()
|
||||
@@ -226,31 +232,37 @@ export async function execPodStep(
|
||||
stdin?: stream.Readable
|
||||
): Promise<void> {
|
||||
const exec = new k8s.Exec(kc)
|
||||
await new Promise(async function (resolve, reject) {
|
||||
await exec.exec(
|
||||
namespace(),
|
||||
podName,
|
||||
containerName,
|
||||
command,
|
||||
process.stdout,
|
||||
process.stderr,
|
||||
stdin ?? null,
|
||||
false /* tty */,
|
||||
resp => {
|
||||
// kube.exec returns an error if exit code is not 0, but we can't actually get the exit code
|
||||
if (resp.status === 'Success') {
|
||||
resolve(resp.code)
|
||||
} else {
|
||||
core.debug(
|
||||
JSON.stringify({
|
||||
message: resp?.message,
|
||||
details: resp?.details
|
||||
})
|
||||
)
|
||||
reject(resp?.message)
|
||||
command = fixArgs(command)
|
||||
// Exec returns a websocket. If websocket fails, we should reject the promise. Otherwise, websocket will call a callback. Since at that point, websocket is not failing, we can safely resolve or reject the promise.
|
||||
await new Promise(function (resolve, reject) {
|
||||
exec
|
||||
.exec(
|
||||
namespace(),
|
||||
podName,
|
||||
containerName,
|
||||
command,
|
||||
process.stdout,
|
||||
process.stderr,
|
||||
stdin ?? null,
|
||||
false /* tty */,
|
||||
resp => {
|
||||
// kube.exec returns an error if exit code is not 0, but we can't actually get the exit code
|
||||
if (resp.status === 'Success') {
|
||||
resolve(resp.code)
|
||||
} else {
|
||||
core.debug(
|
||||
JSON.stringify({
|
||||
message: resp?.message,
|
||||
details: resp?.details
|
||||
})
|
||||
)
|
||||
reject(resp?.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
// If exec.exec fails, explicitly reject the outer promise
|
||||
// eslint-disable-next-line github/no-then
|
||||
.catch(e => reject(e))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -445,7 +457,6 @@ export async function getPodLogs(
|
||||
|
||||
const r = await log.log(namespace(), podName, containerName, logStream, {
|
||||
follow: true,
|
||||
tailLines: 50,
|
||||
pretty: false,
|
||||
timestamps: false
|
||||
})
|
||||
@@ -533,6 +544,26 @@ async function getCurrentNodeName(): Promise<string> {
|
||||
return nodeName
|
||||
}
|
||||
|
||||
async function getPodAffinity(nodeName: string): Promise<k8s.V1Affinity> {
|
||||
const affinity = new k8s.V1Affinity()
|
||||
affinity.nodeAffinity = new k8s.V1NodeAffinity()
|
||||
affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution =
|
||||
new k8s.V1NodeSelector()
|
||||
affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms =
|
||||
[
|
||||
{
|
||||
matchExpressions: [
|
||||
{
|
||||
key: 'kubernetes.io/hostname',
|
||||
operator: 'In',
|
||||
values: [nodeName]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return affinity
|
||||
}
|
||||
|
||||
export function namespace(): string {
|
||||
if (process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE']) {
|
||||
return process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE']
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Mount } from 'hooklib'
|
||||
import * as path from 'path'
|
||||
import { v1 as uuidv4 } from 'uuid'
|
||||
import { POD_VOLUME_NAME } from './index'
|
||||
import { JOB_CONTAINER_EXTENSION_NAME } from '../hooks/constants'
|
||||
import { CONTAINER_EXTENSION_PREFIX } from '../hooks/constants'
|
||||
import * as shlex from 'shlex'
|
||||
|
||||
export const DEFAULT_CONTAINER_ENTRY_POINT_ARGS = [`-f`, `/dev/null`]
|
||||
@@ -41,6 +41,16 @@ export function containerVolumes(
|
||||
name: POD_VOLUME_NAME,
|
||||
mountPath: '/github/file_commands',
|
||||
subPath: '_temp/_runner_file_commands'
|
||||
},
|
||||
{
|
||||
name: POD_VOLUME_NAME,
|
||||
mountPath: '/github/home',
|
||||
subPath: '_temp/_github_home'
|
||||
},
|
||||
{
|
||||
name: POD_VOLUME_NAME,
|
||||
mountPath: '/github/workflow',
|
||||
subPath: '_temp/_github_workflow'
|
||||
}
|
||||
)
|
||||
return mounts
|
||||
@@ -180,7 +190,7 @@ export function mergeContainerWithOptions(
|
||||
): void {
|
||||
for (const [key, value] of Object.entries(from)) {
|
||||
if (key === 'name') {
|
||||
if (value !== base.name && value !== JOB_CONTAINER_EXTENSION_NAME) {
|
||||
if (value !== CONTAINER_EXTENSION_PREFIX + base.name) {
|
||||
core.warning("Skipping name override: name can't be overwritten")
|
||||
}
|
||||
continue
|
||||
@@ -209,7 +219,9 @@ export function mergePodSpecWithOptions(
|
||||
for (const [key, value] of Object.entries(from)) {
|
||||
if (key === 'containers') {
|
||||
base.containers.push(
|
||||
...from.containers.filter(e => !e.name?.startsWith('$'))
|
||||
...from.containers.filter(
|
||||
e => !e.name?.startsWith(CONTAINER_EXTENSION_PREFIX)
|
||||
)
|
||||
)
|
||||
} else if (key === 'volumes' && value) {
|
||||
const volumes = value as k8s.V1Volume[]
|
||||
|
||||
@@ -185,6 +185,20 @@ describe('k8s utils', () => {
|
||||
expect(volumes.find(e => e.mountPath === '/__w')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should always have /github/workflow mount if working on container job or container action', () => {
|
||||
let volumes = containerVolumes([], true, true)
|
||||
expect(volumes.find(e => e.mountPath === '/github/workflow')).toBeTruthy()
|
||||
volumes = containerVolumes([], true, false)
|
||||
expect(volumes.find(e => e.mountPath === '/github/workflow')).toBeTruthy()
|
||||
volumes = containerVolumes([], false, true)
|
||||
expect(volumes.find(e => e.mountPath === '/github/workflow')).toBeTruthy()
|
||||
|
||||
volumes = containerVolumes([], false, false)
|
||||
expect(
|
||||
volumes.find(e => e.mountPath === '/github/workflow')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should have container action volumes', () => {
|
||||
let volumes = containerVolumes([], true, true)
|
||||
let workspace = volumes.find(e => e.mountPath === '/github/workspace')
|
||||
@@ -205,11 +219,10 @@ describe('k8s utils', () => {
|
||||
expect(fileCommands?.subPath).toBe('_temp/_runner_file_commands')
|
||||
})
|
||||
|
||||
it('should have externals, github home and github workflow mounts if job container', () => {
|
||||
it('should have externals, github home mounts if job container', () => {
|
||||
const volumes = containerVolumes()
|
||||
expect(volumes.find(e => e.mountPath === '/__e')).toBeTruthy()
|
||||
expect(volumes.find(e => e.mountPath === '/github/home')).toBeTruthy()
|
||||
expect(volumes.find(e => e.mountPath === '/github/workflow')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw if user volume source volume path is not in workspace', () => {
|
||||
@@ -381,7 +394,7 @@ metadata:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: node:14.16
|
||||
image: node:22
|
||||
- name: job
|
||||
image: ubuntu:latest`
|
||||
)
|
||||
@@ -394,7 +407,7 @@ spec:
|
||||
|
||||
it('should merge container spec', () => {
|
||||
const base = {
|
||||
image: 'node:14.16',
|
||||
image: 'node:22',
|
||||
name: 'test',
|
||||
env: [
|
||||
{
|
||||
@@ -449,7 +462,7 @@ spec:
|
||||
const base = {
|
||||
containers: [
|
||||
{
|
||||
image: 'node:14.16',
|
||||
image: 'node:22',
|
||||
name: 'test',
|
||||
env: [
|
||||
{
|
||||
|
||||
@@ -62,6 +62,54 @@ describe('Prepare job', () => {
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('should prepare job with envs CI and GITHUB_ACTIONS', async () => {
|
||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
|
||||
const content = JSON.parse(
|
||||
fs.readFileSync(prepareJobOutputFilePath).toString()
|
||||
)
|
||||
|
||||
const got = await getPodByName(content.state.jobPod)
|
||||
expect(got.spec?.containers[0].env).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'CI', value: 'true' },
|
||||
{ name: 'GITHUB_ACTIONS', value: 'true' }
|
||||
])
|
||||
)
|
||||
expect(got.spec?.containers[1].env).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'CI', value: 'true' },
|
||||
{ name: 'GITHUB_ACTIONS', value: 'true' }
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('should not override CI env var if already set', async () => {
|
||||
prepareJobData.args.container.environmentVariables = {
|
||||
CI: 'false'
|
||||
}
|
||||
|
||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
|
||||
const content = JSON.parse(
|
||||
fs.readFileSync(prepareJobOutputFilePath).toString()
|
||||
)
|
||||
|
||||
const got = await getPodByName(content.state.jobPod)
|
||||
expect(got.spec?.containers[0].env).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'CI', value: 'false' },
|
||||
{ name: 'GITHUB_ACTIONS', value: 'true' }
|
||||
])
|
||||
)
|
||||
expect(got.spec?.containers[1].env).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'CI', value: 'true' },
|
||||
{ name: 'GITHUB_ACTIONS', value: 'true' }
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an exception if the user volume mount is absolute path outside of GITHUB_WORKSPACE', async () => {
|
||||
prepareJobData.args.container.userMountVolumes = [
|
||||
{
|
||||
@@ -91,6 +139,15 @@ describe('Prepare job', () => {
|
||||
expect(services[0].args).toBe(undefined)
|
||||
})
|
||||
|
||||
it('should determine alpine correctly', async () => {
|
||||
prepareJobData.args.container.image = 'alpine:latest'
|
||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
const content = JSON.parse(
|
||||
fs.readFileSync(prepareJobOutputFilePath).toString()
|
||||
)
|
||||
expect(content.isAlpine).toBe(true)
|
||||
})
|
||||
|
||||
it('should run pod with extensions applied', async () => {
|
||||
process.env[ENV_HOOK_TEMPLATE_PATH] = path.join(
|
||||
__dirname,
|
||||
@@ -116,7 +173,7 @@ describe('Prepare job', () => {
|
||||
|
||||
// job container
|
||||
expect(got.spec?.containers[0].name).toBe(JOB_CONTAINER_NAME)
|
||||
expect(got.spec?.containers[0].image).toBe('node:14.16')
|
||||
expect(got.spec?.containers[0].image).toBe('node:22')
|
||||
expect(got.spec?.containers[0].command).toEqual(['sh'])
|
||||
expect(got.spec?.containers[0].args).toEqual(['-c', 'sleep 50'])
|
||||
|
||||
@@ -124,6 +181,17 @@ describe('Prepare job', () => {
|
||||
expect(got.spec?.containers[1].image).toBe('redis')
|
||||
expect(got.spec?.containers[1].command).toBeFalsy()
|
||||
expect(got.spec?.containers[1].args).toBeFalsy()
|
||||
expect(got.spec?.containers[1].env).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'CI', value: 'true' },
|
||||
{ name: 'GITHUB_ACTIONS', value: 'true' },
|
||||
{ name: 'ENV2', value: 'value2' }
|
||||
])
|
||||
)
|
||||
expect(got.spec?.containers[1].resources).toEqual({
|
||||
requests: { memory: '1Mi', cpu: '1' },
|
||||
limits: { memory: '1Gi', cpu: '2' }
|
||||
})
|
||||
// side-car
|
||||
expect(got.spec?.containers[2].name).toBe('side-car')
|
||||
expect(got.spec?.containers[2].image).toBe('ubuntu:latest')
|
||||
@@ -131,6 +199,26 @@ describe('Prepare job', () => {
|
||||
expect(got.spec?.containers[2].args).toEqual(['-c', 'sleep 60'])
|
||||
})
|
||||
|
||||
it('should put only job and services in output context file', async () => {
|
||||
process.env[ENV_HOOK_TEMPLATE_PATH] = path.join(
|
||||
__dirname,
|
||||
'../../../examples/extension.yaml'
|
||||
)
|
||||
|
||||
await expect(
|
||||
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
const content = JSON.parse(
|
||||
fs.readFileSync(prepareJobOutputFilePath).toString()
|
||||
)
|
||||
|
||||
expect(content.state.jobPod).toBeTruthy()
|
||||
expect(content.context.container).toBeTruthy()
|
||||
expect(content.context.services).toBeTruthy()
|
||||
expect(content.context.services.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should not throw exception using kube scheduler', async () => {
|
||||
// only for ReadWriteMany volumes or single node cluster
|
||||
process.env[ENV_USE_KUBE_SCHEDULER] = 'true'
|
||||
|
||||
@@ -78,4 +78,15 @@ describe('Run container step', () => {
|
||||
runContainerStep(runContainerStepData.args)
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('should run container step with envs CI and GITHUB_ACTIONS', async () => {
|
||||
runContainerStepData.args.entryPoint = 'bash'
|
||||
runContainerStepData.args.entryPointArgs = [
|
||||
'-c',
|
||||
"'if [[ -z $GITHUB_ACTIONS ]] || [[ -z $CI ]]; then exit 1; fi'"
|
||||
]
|
||||
await expect(
|
||||
runContainerStep(runContainerStepData.args)
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
<!-- ## Features -->
|
||||
## Features
|
||||
|
||||
- k8s: Use pod affinity when KubeScheduler is enabled [#212]
|
||||
- docker: support alternative network modes [#209]
|
||||
|
||||
## Bugs
|
||||
|
||||
- Add option to use the kubernetes scheduler for workflow pods [#111]
|
||||
- Docker and K8s: Fix shell arguments when split by the runner [#115]
|
||||
- Expose CI=true and GITHUB_ACTIONS env variables [#215]
|
||||
- k8s: add /github/home to containerAction mounts and surface createSecretForEnvs errors [#198]
|
||||
- k8s: start logging from the beginning [#184]
|
||||
|
||||
## Misc
|
||||
|
||||
- Bump node in tests to node 22 since node14 is quite old [#216]
|
||||
- Bump jsonpath-plus from 10.1.0 to 10.3.0 in /packages/k8s [#213]
|
||||
- Bump braces from 3.0.2 to 3.0.3 in /packages/hooklib [#194]
|
||||
- Bump cross-spawn from 7.0.3 to 7.0.6 in /packages/k8s [#196]
|
||||
- Bump ws from 7.5.8 to 7.5.10 in /packages/k8s [#192]
|
||||
- Remove dependency on deprecated release actions [#193]
|
||||
- Update to the latest available actions [#191]
|
||||
|
||||
<!-- ## Misc -->
|
||||
|
||||
## SHA-256 Checksums
|
||||
|
||||
|
||||
Reference in New Issue
Block a user