devops-platform shared library: reusable pipeline for any module
This commit is contained in:
commit
ed6a8560c5
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# devops-platform — Jenkins Shared Library
|
||||
|
||||
Kisi bhi module ke liye ek hi pipeline. Module ki `Jenkinsfile` sirf itni:
|
||||
|
||||
```groovy
|
||||
@Library('devops-platform') _
|
||||
devopsPipeline(module: 'demo-app')
|
||||
```
|
||||
|
||||
## Kya-kya milta hai (sab default ON)
|
||||
|
||||
| Stage | Tool | Fail hoga jab |
|
||||
|---|---|---|
|
||||
| Secret scan | gitleaks | poori git history me koi secret mila |
|
||||
| Test | module ka test image | test fail |
|
||||
| SAST | SonarQube | quality gate (Sonar side) |
|
||||
| IaC/Dockerfile scan | trivy config | HIGH/CRITICAL misconfig |
|
||||
| CVE gate | trivy image | HIGH/CRITICAL fixable CVE |
|
||||
| SBOM | syft | — (artifact archive hota hai) |
|
||||
| Smoke test | container | `/version` galat |
|
||||
| Push | docker | — |
|
||||
| Sign | cosign | — |
|
||||
| Verify signature | cosign | signature nahi mili |
|
||||
| Deploy + Verify | ansible | playbook fail ya version mismatch |
|
||||
|
||||
## Options
|
||||
|
||||
| Key | Default | Kaam |
|
||||
|---|---|---|
|
||||
| `module` | **zaroori** | module ka naam |
|
||||
| `imageName` | `swim/<module>` | registry me image ka naam |
|
||||
| `vaultMount` | `swim` | Vault KV mount jahan `registry` secret hai |
|
||||
| `testImage` | `python:3.12-slim` | test kis image me chalein |
|
||||
| `testCommand` | pytest | test command |
|
||||
| `trivySeverity` | `HIGH,CRITICAL` | CVE gate ki severity |
|
||||
| `appPort` / `healthPath` | `8000` / `/version` | verify endpoint |
|
||||
| `sonarKey` | `swim-<module>` | SonarQube project key |
|
||||
| `inventory` / `playbook` | `deploy/inventory.ini` / `deploy/deploy.yml` | Ansible |
|
||||
| `runSonar` / `runDeploy` / `approval` | `true` | stage on/off |
|
||||
|
||||
## Branch ka niyam
|
||||
- `main`/`master` → poora raasta (staging → approval → production)
|
||||
- `PR-*` aur baaki branches → **sirf CI gates**, koi deploy nahi
|
||||
|
||||
## Rollback
|
||||
`ROLLBACK_TAG=7` do → build/test/scan skip, seedha us image par deploy.
|
||||
|
||||
## Kahan chalta hai
|
||||
- **CI** → `builder-vm-01` (label `builder`). Yahan deploy key **nahi** hai.
|
||||
- **CD + signing** → Jenkins controller (label `built-in`). Yahan docker.sock **nahi** hai.
|
||||
23
vars/ansibleDeploy.groovy
Normal file
23
vars/ansibleDeploy.groovy
Normal file
@ -0,0 +1,23 @@
|
||||
// Ansible se deploy. Deploy key sirf yahan bind hoti hai aur log me masked rehti hai.
|
||||
def call(String target) {
|
||||
checkout scm
|
||||
withCredentials([
|
||||
sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'DEPLOY_KEY', usernameVariable: 'DEPLOY_USER'),
|
||||
string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')
|
||||
]) {
|
||||
withEnv(["TARGET=${target}"]) {
|
||||
sh '''
|
||||
set +x
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
"$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > /tmp/reg.$$.json
|
||||
export REG_USER=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['username'])")
|
||||
export REG_PASS=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['password'])")
|
||||
rm -f /tmp/reg.$$.json
|
||||
export ANSIBLE_HOST_KEY_CHECKING=False
|
||||
ansible-galaxy collection install community.docker --force -q || true
|
||||
ansible-playbook -i "$INVENTORY" "$PLAYBOOK" \
|
||||
-u "$DEPLOY_USER" --private-key "$DEPLOY_KEY" -e target="$TARGET"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
34
vars/cosignSign.groovy
Normal file
34
vars/cosignSign.groovy
Normal file
@ -0,0 +1,34 @@
|
||||
// Image ko sign karta hai. Key Vault se aati hai aur sirf controller par rehti hai.
|
||||
// Signature registry me ek alag tag ban ke jaati hai.
|
||||
def call() {
|
||||
withCredentials([string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')]) {
|
||||
sh '''
|
||||
set +x
|
||||
umask 077
|
||||
WORK=$(mktemp -d); trap "rm -rf $WORK" EXIT
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/platform/data/cosign" > $WORK/c.json
|
||||
python3 - "$WORK" <<'PY'
|
||||
import json, sys, os
|
||||
w = sys.argv[1]
|
||||
d = json.load(open(w + '/c.json'))['data']['data']
|
||||
for k, f in (('private_key','cosign.key'), ('public_key','cosign.pub'), ('password','pw')):
|
||||
open(os.path.join(w, f), 'w').write(d[k])
|
||||
PY
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > $WORK/r.json
|
||||
python3 - "$WORK" "$REGISTRY" <<'PY'
|
||||
import base64, json, os, sys
|
||||
w, reg = sys.argv[1], sys.argv[2]
|
||||
d = json.load(open(w + '/r.json'))['data']['data']
|
||||
auth = base64.b64encode(('%s:%s' % (d['username'], d['password'])).encode()).decode()
|
||||
os.makedirs(os.path.expanduser('~/.docker'), exist_ok=True)
|
||||
json.dump({'auths': {reg: {'auth': auth}}}, open(os.path.expanduser('~/.docker/config.json'), 'w'))
|
||||
PY
|
||||
# tlog-upload=false: private registry ke digests public transparency log
|
||||
# me nahi bhejte. Verify bhi isi tarah offline hoti hai.
|
||||
COSIGN_PASSWORD=$(cat $WORK/pw) cosign sign --key $WORK/cosign.key \
|
||||
--yes --tlog-upload=false "$FULL_IMAGE"
|
||||
rm -f ~/.docker/config.json
|
||||
echo " signed: $FULL_IMAGE"
|
||||
'''
|
||||
}
|
||||
}
|
||||
28
vars/cosignVerify.groovy
Normal file
28
vars/cosignVerify.groovy
Normal file
@ -0,0 +1,28 @@
|
||||
// Deploy se pehle GATE: signature nahi mili to yahin ruk jao.
|
||||
// Isse koi manually push kiya hua image production me nahi ja sakta.
|
||||
def call() {
|
||||
withCredentials([string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')]) {
|
||||
sh '''
|
||||
set +x
|
||||
umask 077
|
||||
WORK=$(mktemp -d); trap "rm -rf $WORK" EXIT
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/platform/data/cosign" > $WORK/c.json
|
||||
python3 -c "
|
||||
import json,sys
|
||||
d=json.load(open('$WORK/c.json'))['data']['data']
|
||||
open('$WORK/cosign.pub','w').write(d['public_key'])"
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > $WORK/r.json
|
||||
python3 - "$WORK" "$REGISTRY" <<'PY'
|
||||
import base64, json, os, sys
|
||||
w, reg = sys.argv[1], sys.argv[2]
|
||||
d = json.load(open(w + '/r.json'))['data']['data']
|
||||
auth = base64.b64encode(('%s:%s' % (d['username'], d['password'])).encode()).decode()
|
||||
os.makedirs(os.path.expanduser('~/.docker'), exist_ok=True)
|
||||
json.dump({'auths': {reg: {'auth': auth}}}, open(os.path.expanduser('~/.docker/config.json'), 'w'))
|
||||
PY
|
||||
cosign verify --key $WORK/cosign.pub --insecure-ignore-tlog=true "$FULL_IMAGE" \
|
||||
&& echo " signature OK: $FULL_IMAGE"
|
||||
rm -f ~/.docker/config.json
|
||||
'''
|
||||
}
|
||||
}
|
||||
252
vars/devopsPipeline.groovy
Normal file
252
vars/devopsPipeline.groovy
Normal file
@ -0,0 +1,252 @@
|
||||
// Ek hi pipeline, har module ke liye. Module ki Jenkinsfile sirf itni hoti hai:
|
||||
//
|
||||
// @Library('devops-platform') _
|
||||
// devopsPipeline(module: 'demo-app')
|
||||
//
|
||||
// Poora raasta: secret scan -> test -> SAST -> build -> IaC scan -> CVE gate ->
|
||||
// SBOM -> smoke -> push -> sign -> verify -> staging -> approval -> production.
|
||||
//
|
||||
// CI builder VM par chalta hai (wahan deploy key nahi hai),
|
||||
// CD controller par (wahan docker.sock nahi hai). Dono taraf blast radius chhota.
|
||||
|
||||
def call(Map cfg = [:]) {
|
||||
|
||||
def P = platformConfig()
|
||||
if (!cfg.module) { error "devopsPipeline: 'module' dena zaroori hai" }
|
||||
|
||||
// ---- defaults: module kuch na de to sensible value ----
|
||||
cfg.imageName = cfg.imageName ?: "swim/${cfg.module}"
|
||||
cfg.vaultMount = cfg.vaultMount ?: 'swim'
|
||||
cfg.testImage = cfg.testImage ?: 'python:3.12-slim'
|
||||
cfg.testCommand = cfg.testCommand ?: 'pip install --quiet -r requirements-dev.txt && python -m pytest tests/ -v --junitxml=test-results.xml'
|
||||
cfg.trivySeverity = cfg.trivySeverity ?: 'HIGH,CRITICAL'
|
||||
cfg.healthPath = cfg.healthPath ?: '/version'
|
||||
cfg.appPort = cfg.appPort ?: '8000'
|
||||
cfg.sonarKey = cfg.sonarKey ?: "swim-${cfg.module}"
|
||||
cfg.inventory = cfg.inventory ?: 'deploy/inventory.ini'
|
||||
cfg.playbook = cfg.playbook ?: 'deploy/deploy.yml'
|
||||
cfg.runSonar = cfg.containsKey('runSonar') ? cfg.runSonar : true
|
||||
cfg.runDeploy = cfg.containsKey('runDeploy') ? cfg.runDeploy : true
|
||||
cfg.approval = cfg.containsKey('approval') ? cfg.approval : true
|
||||
|
||||
properties([
|
||||
parameters([
|
||||
string(name: 'ROLLBACK_TAG', defaultValue: '',
|
||||
description: 'Khaali = normal build. Purana tag (jaise 7) = bina rebuild seedha us image par rollback.')
|
||||
]),
|
||||
buildDiscarder(logRotator(numToKeepStr: '30')),
|
||||
disableConcurrentBuilds()
|
||||
])
|
||||
|
||||
def branch = env.BRANCH_NAME ?: 'main'
|
||||
def isPR = branch.startsWith('PR-')
|
||||
// PR par sirf CI. Production sirf main/master se jaata hai.
|
||||
def deployable = cfg.runDeploy && !isPR && (branch == 'main' || branch == 'master')
|
||||
|
||||
def rollbackTag = (env.ROLLBACK_TAG ?: '').trim()
|
||||
def isRollback = rollbackTag != ''
|
||||
def imageTag = isRollback ? rollbackTag : "${env.BUILD_NUMBER}"
|
||||
def fullImage = "${P.registry}/${cfg.imageName}:${imageTag}"
|
||||
|
||||
def commonEnv = [
|
||||
"MODULE=${cfg.module}",
|
||||
"REGISTRY=${P.registry}",
|
||||
"IMAGE_NAME=${cfg.imageName}",
|
||||
"IMAGE_TAG=${imageTag}",
|
||||
"FULL_IMAGE=${fullImage}",
|
||||
"VAULT_ADDR=${P.vaultAddr}",
|
||||
"VAULT_MOUNT=${cfg.vaultMount}",
|
||||
"SONAR_URL=${P.sonarUrl}",
|
||||
"SONAR_KEY=${cfg.sonarKey}",
|
||||
"BRIDGE_IP=${P.bridgeIp}",
|
||||
"TRIVY_SEVERITY=${cfg.trivySeverity}",
|
||||
"APP_PORT=${cfg.appPort}",
|
||||
"HEALTH_PATH=${cfg.healthPath}",
|
||||
"INVENTORY=${cfg.inventory}",
|
||||
"PLAYBOOK=${cfg.playbook}",
|
||||
"IMG_GITLEAKS=${P.gitleaks}",
|
||||
"IMG_TRIVY=${P.trivy}",
|
||||
"IMG_SYFT=${P.syft}",
|
||||
"IMG_SCANNER=${P.sonarScanner}",
|
||||
"TEST_IMAGE=${cfg.testImage}"
|
||||
]
|
||||
|
||||
timestamps {
|
||||
timeout(time: 60, unit: 'MINUTES') {
|
||||
try {
|
||||
|
||||
// ===================== CI: builder VM =====================
|
||||
node(P.builderLabel) {
|
||||
withEnv(commonEnv) {
|
||||
try {
|
||||
stage('Init') {
|
||||
echo "module=${cfg.module} node=${env.NODE_NAME} branch=${branch} " +
|
||||
"mode=${isRollback ? 'ROLLBACK' : 'BUILD'} tag=${imageTag} deploy=${deployable}"
|
||||
}
|
||||
|
||||
stage('Checkout') {
|
||||
checkout scm
|
||||
sh 'echo "commit: $(git rev-parse --short HEAD)"'
|
||||
}
|
||||
|
||||
stage('Secret scan') {
|
||||
// Poori git history scan hoti hai, sirf latest commit nahi -
|
||||
// purana leak bhi pakda jaata hai. Findings = build fail.
|
||||
sh '''
|
||||
docker run --rm -v "$PWD":/repo "$IMG_GITLEAKS" \
|
||||
detect --source=/repo --redact --no-banner -v
|
||||
'''
|
||||
}
|
||||
|
||||
if (!isRollback) {
|
||||
stage('Test') {
|
||||
sh """
|
||||
docker run --rm -v "\$PWD":/src -w /src "\$TEST_IMAGE" sh -c '${cfg.testCommand}'
|
||||
"""
|
||||
junit allowEmptyResults: true, testResults: 'test-results.xml'
|
||||
}
|
||||
|
||||
if (cfg.runSonar) {
|
||||
stage('SAST (SonarQube)') {
|
||||
withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
|
||||
sh '''
|
||||
set +x
|
||||
docker run --rm \
|
||||
--add-host "ns31240276.ip-51-195-4.eu:$BRIDGE_IP" \
|
||||
-e SONAR_HOST_URL="$SONAR_URL" \
|
||||
-e SONAR_TOKEN="$SONAR_TOKEN" \
|
||||
-v "$PWD":/usr/src "$IMG_SCANNER" \
|
||||
-Dsonar.projectKey="$SONAR_KEY"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('IaC / Dockerfile scan') {
|
||||
// Dockerfile, compose, k8s manifests ki misconfig pakadta hai
|
||||
// (root user, missing healthcheck, privileged, etc.)
|
||||
sh '''
|
||||
docker run --rm -v "$PWD":/work "$IMG_TRIVY" config /work \
|
||||
--severity "$TRIVY_SEVERITY" --exit-code 1 --no-progress
|
||||
'''
|
||||
}
|
||||
|
||||
stage('Build image') {
|
||||
sh '''
|
||||
docker build --build-arg APP_VERSION="$IMAGE_TAG" \
|
||||
-t "$FULL_IMAGE" -t "$REGISTRY/$IMAGE_NAME:latest" .
|
||||
'''
|
||||
}
|
||||
|
||||
stage('CVE gate (Trivy)') {
|
||||
sh '''
|
||||
docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v trivy-cache:/root/.cache/ \
|
||||
-v "$PWD/.trivyignore":/.trivyignore:ro \
|
||||
"$IMG_TRIVY" image \
|
||||
--severity "$TRIVY_SEVERITY" --ignore-unfixed \
|
||||
--ignorefile /.trivyignore --exit-code 1 \
|
||||
--format table --no-progress "$FULL_IMAGE"
|
||||
'''
|
||||
}
|
||||
|
||||
stage('SBOM') {
|
||||
// Audit ka pehla sawaal: "is image me kya-kya hai?"
|
||||
sh '''
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
"$IMG_SYFT" "docker:$FULL_IMAGE" -o spdx-json > sbom.spdx.json
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
"$IMG_SYFT" "docker:$FULL_IMAGE" -o cyclonedx-json > sbom.cyclonedx.json
|
||||
echo " SBOM packages: $(python3 -c "import json;print(len(json.load(open('sbom.spdx.json'))['packages']))")"
|
||||
'''
|
||||
archiveArtifacts artifacts: 'sbom.*.json', fingerprint: true
|
||||
}
|
||||
|
||||
stage('Smoke test') {
|
||||
sh '''
|
||||
CID=$(docker run -d -P "$FULL_IMAGE")
|
||||
trap "docker rm -f $CID >/dev/null 2>&1" EXIT
|
||||
sleep 5
|
||||
docker exec $CID python -c "
|
||||
import urllib.request, json
|
||||
r = json.loads(urllib.request.urlopen('http://127.0.0.1:${APP_PORT}${HEALTH_PATH}').read())
|
||||
print(' live:', r)
|
||||
assert r['version'] == '${IMAGE_TAG}', 'version mismatch'
|
||||
"
|
||||
'''
|
||||
}
|
||||
|
||||
stage('Push') {
|
||||
withCredentials([string(credentialsId: 'vault-builder-token', variable: 'VAULT_TOKEN')]) {
|
||||
sh '''
|
||||
set +x
|
||||
curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
"$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > /tmp/reg.$$.json
|
||||
U=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['username'])")
|
||||
P=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['password'])")
|
||||
rm -f /tmp/reg.$$.json
|
||||
echo "$P" | docker login "$REGISTRY" -u "$U" --password-stdin
|
||||
docker push "$FULL_IMAGE"
|
||||
docker push "$REGISTRY/$IMAGE_NAME:latest"
|
||||
docker logout "$REGISTRY"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sh 'docker image prune -f --filter "until=24h" >/dev/null 2>&1 || true'
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Sign: controller par (key builder ko nahi milti) ============
|
||||
if (!isRollback) {
|
||||
node(P.deployLabel) {
|
||||
withEnv(commonEnv) {
|
||||
stage('Sign image') { cosignSign() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!deployable) {
|
||||
echo isPR ? "PR build - deploy nahi hoga, sirf CI gates." : "Branch '${branch}' deployable nahi hai."
|
||||
return
|
||||
}
|
||||
|
||||
// ===================== CD: controller =====================
|
||||
node(P.deployLabel) {
|
||||
withEnv(commonEnv) {
|
||||
stage('Verify signature') { cosignVerify() }
|
||||
stage('Deploy STAGING') { ansibleDeploy('staging') }
|
||||
stage('Verify STAGING') { verifyEnv('staging') }
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.approval) {
|
||||
stage('Production approval') {
|
||||
// Node ke bahar hai - intezaar me koi executor block nahi hota.
|
||||
timeout(time: 30, unit: 'MINUTES') {
|
||||
input message: "${cfg.module}:${imageTag} staging par verified hai. PRODUCTION par bhejein?",
|
||||
ok: 'Deploy to Production'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node(P.deployLabel) {
|
||||
withEnv(commonEnv) {
|
||||
stage('Deploy PRODUCTION') { ansibleDeploy('production') }
|
||||
stage('Verify PRODUCTION') { verifyEnv('production') }
|
||||
}
|
||||
}
|
||||
|
||||
currentBuild.description = "${cfg.module}:${imageTag}${isRollback ? ' (rollback)' : ''}"
|
||||
|
||||
} catch (e) {
|
||||
currentBuild.result = 'FAILURE'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
vars/platformConfig.groovy
Normal file
20
vars/platformConfig.groovy
Normal file
@ -0,0 +1,20 @@
|
||||
// Platform ke sab endpoints ek jagah. Kuch badle to sirf yahan badlo,
|
||||
// har module ki pipeline apne aap nayi value uthayegi.
|
||||
def call() {
|
||||
return [
|
||||
host : 'ns31240276.ip-51-195-4.eu',
|
||||
registry : 'ns31240276.ip-51-195-4.eu:5000',
|
||||
giteaUrl : 'https://ns31240276.ip-51-195-4.eu',
|
||||
vaultAddr : 'https://ns31240276.ip-51-195-4.eu:8444',
|
||||
sonarUrl : 'https://ns31240276.ip-51-195-4.eu:8446',
|
||||
// VM ke andar se host is IP par milta hai (libvirt bridge)
|
||||
bridgeIp : '192.168.124.1',
|
||||
builderLabel: 'builder',
|
||||
deployLabel : 'built-in',
|
||||
// pinned versions - "latest" kabhi nahi, warna build reproducible nahi rehti
|
||||
gitleaks : 'zricethezav/gitleaks:v8.21.2',
|
||||
trivy : 'aquasec/trivy:0.58.1',
|
||||
syft : 'anchore/syft:v1.18.1',
|
||||
sonarScanner: 'sonarsource/sonar-scanner-cli:11'
|
||||
]
|
||||
}
|
||||
18
vars/verifyEnv.groovy
Normal file
18
vars/verifyEnv.groovy
Normal file
@ -0,0 +1,18 @@
|
||||
// Deploy ke baad sach me chal raha hai ya nahi - live endpoint se check.
|
||||
def call(String target) {
|
||||
withEnv(["TARGET=${target}"]) {
|
||||
sh '''
|
||||
HOSTIP=$(awk -v t="[$TARGET]" '
|
||||
$0 == t {f=1; next}
|
||||
/^\\[/ {f=0}
|
||||
f && /ansible_host=/ {sub(/.*ansible_host=/,""); print $1; exit}
|
||||
' "$INVENTORY")
|
||||
[ -n "$HOSTIP" ] || { echo "$TARGET ka host inventory me nahi mila"; exit 1; }
|
||||
GOT=$(curl -sf --max-time 15 "http://$HOSTIP:$APP_PORT$HEALTH_PATH" \
|
||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['version'])")
|
||||
echo " $TARGET ($HOSTIP) expected=$IMAGE_TAG live=$GOT"
|
||||
[ "$GOT" = "$IMAGE_TAG" ] || { echo " $TARGET MISMATCH"; exit 1; }
|
||||
echo " $TARGET VERIFIED"
|
||||
'''
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user