jpgboost-cli से अपनी CI पाइपलाइन में इमेज कम्प्रेशन कैसे ऑटोमेट करें
जिस रिपॉज़िटरी में बिना कम्प्रेस की गई JPEG और PNG जमा होती जाती हैं, वह LCP को बिगाड़ती है और साथ ही हर क्लोन को भारी बना देती है। jpgboost-cli से इनका कम्प्रेशन सीधे आपकी CI पाइपलाइन में ऑटोमेट किया जा सकता है, बिना हर योगदानकर्ता की सतर्कता पर निर्भर हुए। यह लेख GitLab CI के साथ इसका एकीकरण दिखाता है, दो विपरीत रणनीतियों की तुलना करता है, और उन मुश्किलों पर बात करता है जो प्रोडक्शन में महँगी पड़ सकती हैं।
बिना कम्प्रेस की गई इमेज की असली कीमत
बिना कम्प्रेस किए कमिट की गई हर इमेज Git हिस्ट्री में एक blob के रूप में अपना निशान छोड़ जाती है, जिसमें उसकी पूरी सामग्री होती है। बाद में उस इमेज को हल्के संस्करण से बदल देने पर भी पुराना blob हिस्ट्री में बना रहता है। git clone उस हिस्ट्री को भी साथ ले आता है। इसलिए पुरानी इमेज का वज़न रिपॉज़िटरी पर पड़ता रहता है, भले ही वे अब इस्तेमाल में न हों।
जिस रिपॉज़िटरी में कई महीनों तक स्क्रीनशॉट, मार्केटिंग सामग्री या डिज़ाइन एक्सपोर्ट जमा होते रहते हैं, वहाँ यह बेकार वज़न जल्दी ही कई सौ मेगाबाइट तक पहुँच जाता है। इससे CI में क्लोन धीमे होते हैं और हर योगदानकर्ता की बैंडविड्थ खपत बढ़ती है।
प्रोडक्शन की तरफ़ समस्या रिपॉज़िटरी पर ख़त्म नहीं होती। ज़रूरत से बड़ी इमेज सीधे Largest Contentful Paint (LCP) को नुक़सान पहुँचाती है, ख़ासकर तब जब पहली स्क्रीन की सबसे बड़ी इमेज ही वह तत्व बन जाए जो इस मीट्रिक को तय करता है। कम्प्रेशन से मिलने वाला फ़ायदा काफ़ी बड़ा हो सकता है: jpgboost-cli के दस्तावेज़ों के अपने उदाहरण में 4.2 MB की एक JPEG घटकर सिर्फ़ 890 KB रह जाती है, यानी 79 % की कमी।
मैन्युअल कम्प्रेशन बड़े पैमाने पर काम नहीं करता। यह व्यक्तिगत आदत पर टिका होता है। हर मर्ज रिक्वेस्ट पर हर योगदानकर्ता को कमिट से पहले इमेज ऑप्टिमाइज़ करना याद रखना पड़ता है। व्यवहार में, जैसे ही समय की कमी या जल्दबाज़ी बढ़ती है, यह क़दम आसानी से छूट जाता है। समस्या आम तौर पर कई हफ़्ते बाद तब पता चलती है, जब कोई ऑडिट या परफ़ॉर्मेंस रिपोर्ट जमा हो चुके कर्ज़ को सामने लाती है।
लोकल या सर्वर के बजाय CI में ऑटोमेट क्यों करें
इमेज को लोकल पर कम्प्रेस करना, जैसे Git के pre-commit हुक से, हमेशा मशीन और योगदानकर्ता के अनुशासन पर निर्भर रहता है। हुक को छोड़ा जा सकता है, हटाया जा सकता है, या वह उस डिज़ाइनर की मशीन पर हो ही न, जो फ़ाइलें सीधे कमिट करता है।
दूसरी ओर, सर्वर पर होने वाला कम्प्रेशन बहुत देर से आता है। इमेज तब तक इंडेक्स हो चुकी होती हैं और शायद कम से कम एक बार परोसी भी जा चुकी होती हैं। यानी समस्या शुरुआती विज़िटर को दिख जाती है, और उसके बाद हर डिप्लॉय पर ऑप्टिमाइज़ेशन फिर से करना पड़ता है।
CI सबसे भरोसेमंद जाँच बिंदु है। यह हर मर्ज रिक्वेस्ट पर चलता है, लोकल मशीन की सेटिंग पर निर्भर नहीं करता, और जॉब के लॉग में एक दिखने वाला, ट्रैक किया जा सकने वाला नतीजा छोड़ता है। यही इकलौती अनिवार्य चौकी ऑटोमेशन को सार्थक बनाती है, भले ही इसके साथ ऐसी इन्फ़्रास्ट्रक्चर लागत जुड़ी हो जिसे शुरू से ही हिसाब में रखना ज़रूरी है।
यहाँ यह लागत ज़्यादा भारी पड़ती है, क्योंकि jpgboost-cli कोई ऐसी स्वतंत्र बाइनरी नहीं है जिसे पैकेज मैनेजर से इंस्टॉल किया जा सके। यह JPGBoost.app के साथ आती है, सिर्फ़ macOS 15 या उससे नए पर चलती है, और इसके लिए Pro लाइसेंस चाहिए।
इसलिए इस लेख का बाक़ी हिस्सा एक ठोस मान्यता पर टिका है: एक स्थायी, सेल्फ़-होस्टेड macOS रनर, ताकि माहौल स्थिर रहे और JPGBoost.app की इंस्टॉलेशन एक जॉब से दूसरे जॉब तक बनी रहे।
jpgboost-cli इंस्टॉल करें और लोकल पर जाँचें
रनर पर हो या लोकल जाँच में, बाइनरी ऐप बंडल के भीतर ही होती है। एक बार सिम्बॉलिक लिंक बना लेने पर उसे किसी भी फ़ोल्डर से चलाया जा सकता है।
# एक बार सिम्बॉलिक लिंक, ताकि jpgboost-cli किसी भी फ़ोल्डर से चले
sudo ln -s /Applications/JPGBoost.app/Contents/MacOS/jpgboost-cli /usr/local/bin/jpgboost-cli
# जाँचें कि कमांड जवाब दे रही है
jpgboost-cli --help
इसके बाद Pro लाइसेंस रनर पर एक ही बार, हाथ से सक्रिय किया जाता है। यह कभी भी पाइपलाइन के भीतर नहीं होता। अगर यह इस लाइसेंस को सक्रिय करने वाली पहली इंस्टॉलेशन है, तो ख़रीद के बाद ईमेल से मिला टोकन इस्तेमाल करें।
# इस मशीन की पहचान, ज़रूरत पड़ने पर सपोर्ट को देने के लिए
jpgboost-cli --machine-id
# पहली बार सक्रियण, ख़रीद के बाद ईमेल से मिले टोकन के साथ
jpgboost-cli --activate ACT-XXXX-XXXX-XXXX-XXXX-XXXX
अगर लाइसेंस पहले ही कहीं और सक्रिय किया जा चुका है (आम तौर पर निजी इस्तेमाल के लिए ऐप में), तो --activate अब सही तरीक़ा नहीं है। रनर को नया लाइसेंस सक्रिय करने के बजाय उसी मौजूदा लाइसेंस से जुड़ना चाहिए। पहले से सक्रिय डिवाइस पर एक कोड बनाएँ, फिर उसे रनर पर इस्तेमाल करें।
# पहले से सक्रिय डिवाइस पर (ऐप या कोई दूसरा CLI) एक कोड बनाता है
jpgboost-cli --add-device
# रनर पर, इस कोड से मौजूदा लाइसेंस से जुड़ता है
jpgboost-cli --pair XXXX-XXXX
दोनों में से किसी भी तरीक़े से किया गया यह सक्रियण Pro लाइसेंस में शामिल दो इंस्टॉलेशन में से एक गिना जाता है। चूँकि ऐप और CLI दो अलग इंस्टॉलेशन गिने जाते हैं, हर जॉब पर सक्रिय करने से कोटा जल्दी ख़त्म हो जाएगा: सिर्फ़ दो बार चलाने में ही लाइसेंस पूरी तरह इस्तेमाल हो चुका होगा। यही वजह है कि रनर स्थायी होना चाहिए, ताकि सक्रियण एक जॉब से दूसरे जॉब तक बना रहे।
आगे बढ़ने से पहले लोकल पर एक जल्दी जाँच।
# दो फ़ाइलें क्वालिटी 60 पर WebP में
jpgboost-cli photo1.jpg photo2.png --quality 60 --format webp --output ./compressed
GitLab सेट करें (रनर और पुश टोकन)
नीचे दी गई .gitlab-ci.yml चिपकाने से पहले, GitLab की तरफ़ दो चीज़ें एक बार सेट करनी होती हैं: रनर रजिस्टर करना, और वह टोकन बनाना जिसकी सुधार वाले जॉब को पुश करने के लिए ज़रूरत है।
रनर रजिस्टर करें
जिस Mac को स्थायी रनर के तौर पर चुना है (वही, जहाँ पिछले हिस्से के मुताबिक़ jpgboost-cli और उसका लाइसेंस पहले से इंस्टॉल हैं), उस पर रनर को macos टैग के साथ रजिस्टर करें, जिसे दोनों जॉब इस्तेमाल करते हैं।
gitlab-runner register \
--url https://gitlab.com \
--token <PROJECT_REGISTRATION_TOKEN>
रजिस्ट्रेशन टोकन Settings > CI/CD > Runners > New project runner में मिलता है (इंटरफ़ेस पुराने साझा रजिस्ट्रेशन टोकन के बजाय इस रजिस्ट्रेशन के लिए एक बार इस्तेमाल होने वाला टोकन बनाता है)। यहाँ --executor shell अहम है: Docker एग्ज़ीक्यूटर के उलट, यह कमांड सीधे रनर के सिस्टम पर चलाता है, जो इसलिए ज़रूरी है ताकि हर बार साफ़ माहौल से शुरू करने के बजाय jpgboost-cli पहले से इंस्टॉल और उसका लाइसेंस पहले से सक्रिय एक जॉब से दूसरे जॉब तक मिलता रहे।
gitlab-runner register पहले रनर का नाम, फिर एग्ज़ीक्यूटर इंटरैक्टिव तरीक़े से पूछता है; आख़िरी सवाल का जवाब shell दें
पुश टोकन बनाएँ
fix-image-weight जॉब मर्ज रिक्वेस्ट की ब्रांच पर एक कमिट पुश करता है। हर जॉब को अपने आप मिलने वाले CI_JOB_TOKEN के पास सुरक्षित ब्रांच पर लिखने का ज़रूरी अधिकार नहीं होता, इसलिए एक अलग टोकन चाहिए।
Project Access Token (प्रोजेक्ट का Settings > Access Tokens) सिद्धांत रूप में सबसे साफ़ विकल्प है: यह किसी खाते के बजाय प्रोजेक्ट से जुड़ा होता है, इसलिए बनाने वाले के जाने के बाद भी टिका रहता है। लेकिन मुफ़्त प्लान के निजी नेमस्पेस पर GitLab.com यह सुविधा नहीं देता, यह सिर्फ़ भुगतान वाले ग्रुप के लिए है: इसके बदले Personal Access Token इस्तेमाल करने की सबसे आम वजह यही है।
Personal Access Token प्रोजेक्ट की नहीं, बल्कि यूज़र खाते की सेटिंग्स से बनाया जाता है (Avatar > Edit profile > Access Tokens), इन सेटिंग्स के साथ:
- स्कोप
write_repository - आपकी टोकन रोटेशन नीति से मेल खाती समाप्ति तिथि
यह रास्ता चुनने से पहले एक बात जान लेनी चाहिए: टोकन उसी खाते से जुड़ा होता है जिसने उसे बनाया। अगर वह खाता बंद कर दिया जाए, प्रोजेक्ट तक पहुँच खो दे, या वह व्यक्ति टीम छोड़ दे, तो सुधार वाला जॉब पुश करना बंद कर देगा, और ऐसा होने से पहले कोई चेतावनी नहीं मिलेगी। टीम के प्रोजेक्ट में इसे किसी योगदानकर्ता के निजी खाते के बजाय एक अलग सर्विस खाते से बनाना बेहतर है।
बना हुआ टोकन सिर्फ़ एक बार दिखता है: उसे तुरंत कॉपी करें, फिर प्रोजेक्ट के Settings > CI/CD > Variables में PUSH_TOKEN नाम से जोड़ें। अगर आपकी मर्ज रिक्वेस्ट किसी सुरक्षित ब्रांच को निशाना बनाती हैं, तो Masked और Protected विकल्प चुनें।
PUSH_TOKEN है, जो आगे .gitlab-ci.yml में इस्तेमाल होता हैअगर आपकी मर्ज रिक्वेस्ट की सोर्स ब्रांच ख़ुद भी सुरक्षित है, तो टोकन से जुड़े खाते को उस पर सीधे पुश करने का अधिकार भी चाहिए, Settings > Repository > Protected branches > Allowed to push and merge में; वरना टोकन सही होने के बावजूद जॉब का git push पहुँच अस्वीकार होने से विफल हो जाएगा।
.gitlab-ci.yml फ़ाइल कहाँ रखें
GitLab डिफ़ॉल्ट रूप से पाइपलाइन सिर्फ़ एक जगह ढूँढ़ता है: रिपॉज़िटरी की जड़ में, README के बगल में, ठीक .gitlab-ci.yml नाम की फ़ाइल (शुरुआती बिंदु सहित)। किसी सब-फ़ोल्डर में रखी फ़ाइल, या अलग नाम वाली फ़ाइल, बस अनदेखी कर दी जाती है: कोई त्रुटि इसकी सूचना नहीं देती, प्रोजेक्ट बस ऐसे व्यवहार करता है मानो उसमें कोई CI सेट ही न हो।
अगर किसी और जगह की ज़रूरत हो, जैसे कई रिपॉज़िटरी में साझा की गई पाइपलाइन फ़ाइल या मोनोरेपो ढाँचा, तो Settings > CI/CD > General pipelines > CI/CD configuration file से किसी दूसरे पथ की ओर इशारा किया जा सकता है, यहाँ तक कि किसी दूसरे प्रोजेक्ट में भी, पथ/फ़ाइल.yml@ग्रुप/प्रोजेक्ट:ब्रांच सिंटैक्स के साथ। इस ख़ास स्थिति के अलावा, इस खाने को ख़ाली छोड़ें और फ़ाइल को जड़ में ही रखें।
फ़ाइल कमिट और पुश हो जाने के बाद किसी मैन्युअल सक्रियण की ज़रूरत नहीं: GitLab उसे अगली ऐसी घटना पर अपने आप पहचान लेता है जो फ़ाइल के किसी नियम से मेल खाती हो, यहाँ $CI_PIPELINE_SOURCE == "merge_request_event" के ज़रिए मर्ज रिक्वेस्ट का खुलना या अपडेट होना। नतीजा प्रोजेक्ट के Pipelines टैब में और सीधे मर्ज रिक्वेस्ट के उसी नाम वाले टैब में दिखता है। पहली विफल पाइपलाइन पर पता चलने के बजाय पुश से पहले ही सिंटैक्स जाँचने के लिए, बिल्ट-इन एडिटर (प्रोजेक्ट मेन्यू में Build > Pipeline editor) में Validate बटन है, जो असली पाइपलाइन चलाए बिना GitLab का CI लिंटर बुलाता है।
verification और correction के साथGitLab CI के साथ एकीकरण
नीचे दिया गया जॉब macos टैग वाले रनर को निशाना बनाता है, यानी ऊपर बताए गए उसी स्थायी सेल्फ़-होस्टेड रनर को, जो पाइपलाइन के बाहर पहले ही सक्रिय किया जा चुका है। यह सिर्फ़ मर्ज रिक्वेस्ट की पाइपलाइनों पर चलता है और अगले हिस्से में बताए गए दोनों मोड को जोड़ता है: वज़न की जाँच, और फिर अपने आप सुधार।
# .gitlab-ci.yml
stages:
- verification
- correction
variables:
# वह सीमा जिसके ऊपर कम्प्रेस की गई इमेज जॉब को विफल कर देती है (KB में)
THRESHOLD_KB: "6000"
# पूरा क्लोन: इसके बिना उथले क्लोन में
# CI_MERGE_REQUEST_DIFF_BASE_SHA न हो और नीचे का "git diff" विफल हो सकता है।
GIT_DEPTH: "0"
check-image-weight:
stage: verification
tags: [macos]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# आगे बढ़ने से पहले जाँचें कि jpgboost-cli जवाब दे रहा है
jpgboost-cli --help > /dev/null
# सिर्फ़ इस मर्ज रिक्वेस्ट में जोड़ी या बदली गई इमेज देखें
# (मौजूदा दायरा: .jpg और .png)
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "इस मर्ज रिक्वेस्ट में कोई इमेज नहीं बदली गई।"
exit 0
fi
# आकार की तुलना के लिए एक अस्थायी फ़ोल्डर में WebP में परीक्षण कम्प्रेशन
mkdir -p /tmp/check-images
# ("readarray" नहीं: macOS अब भी bash 3.2 देता है, जहाँ यह बिल्टिन
# मौजूद नहीं, यह सिर्फ़ bash 4 में आया)
FILES_ARR=()
while IFS= read -r LINE; do
FILES_ARR+=("$LINE")
done <<< "$FILES"
jpgboost-cli "${FILES_ARR[@]}" --format webp --quality 75 --jobs 4 --output /tmp/check-images --json > /tmp/report.json
# अगर कम्प्रेस होने के बाद भी कोई इमेज सीमा से ऊपर है तो जॉब विफल कर दें
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
OVER_LIMIT=$(jq --argjson threshold "$THRESHOLD_BYTES" '[.[] | select(.compressedSizeBytes > $threshold)] | length' /tmp/report.json)
if [ "$OVER_LIMIT" -gt 0 ]; then
echo "कम्प्रेस होने के बाद भी $THRESHOLD_KB KB से ऊपर इमेज: $OVER_LIMIT"
jq --argjson threshold "$THRESHOLD_BYTES" -r '.[] | select(.compressedSizeBytes > $threshold) | .path' /tmp/report.json
exit 1
fi
echo "बदली गई सभी इमेज $THRESHOLD_KB KB की सीमा के भीतर हैं।"
fix-image-weight:
stage: correction
tags: [macos]
# "verification" स्टेज से स्वतंत्र: वरना यह जॉब ठीक उसी अकेली स्थिति में
# कभी नहीं पहुँचेगा जहाँ इसकी असल ज़रूरत है (जब check-image-weight विफल हो)।
needs: []
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# सिर्फ़ .jpg/.png, check-image-weight की इसी तरह की टिप्पणी देखें।
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "इस मर्ज रिक्वेस्ट में कोई इमेज नहीं बदली गई।"
exit 0
fi
rm -f /tmp/report.json
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
MIN_QUALITY=20
REMAINING=""
# हर फ़ाइल को JPEG में बदलता है (--format jpeg, यही डिफ़ॉल्ट मान है):
# किसी तस्वीर के लिए PNG, JPEG से काफ़ी ख़राब कम्प्रेस होता है ("आम
# मुश्किलें" देखें), और यहाँ यही लक्ष्य फ़ॉर्मेट अपेक्षित है। पारदर्शिता
# वाली PNG के लिए उपयुक्त नहीं (JPEG में अल्फ़ा चैनल नहीं होता), तस्वीर
# के लिए यह समस्या नहीं, पर अगर कभी कोई लोगो या आइकॉन इस पाइपलाइन से
# गुज़रे तो इस पर फिर से सोचना होगा।
#
# जब तक नतीजा सीमा से ऊपर रहे, क्वालिटी चरणों में घटाता है: तय क्वालिटी
# (60) पर एक ही बार चलाना हमेशा काफ़ी नहीं होता।
while IFS= read -r FILE; do
FOLDER=$(dirname "$FILE")
QUALITY=60
while :; do
RESULT=$(jpgboost-cli "$FILE" --format jpeg --quality "$QUALITY" --output "$FOLDER" --json)
SIZE=$(echo "$RESULT" | jq '.[0].compressedSizeBytes // 0')
if [ "$SIZE" -le "$THRESHOLD_BYTES" ] || [ "$QUALITY" -le "$MIN_QUALITY" ]; then
break
fi
QUALITY=$((QUALITY - 15))
if [ "$QUALITY" -lt "$MIN_QUALITY" ]; then
QUALITY=$MIN_QUALITY
fi
done
echo "$RESULT" >> /tmp/report.json
# आउटपुट फ़ाइल हमेशा <बेस>.jpg होती है: अगर मूल फ़ाइल का पहले से यही
# एक्सटेंशन नहीं था (जैसे .png), तो मूल फ़ाइल को रिपॉज़िटरी से हटाना
# ज़रूरी है, वरना दोनों साथ रहेंगी और पुरानी अनिश्चित काल तक बिना
# कम्प्रेस हुए पड़ी रहेगी।
case "$FILE" in
*.jpg) : ;;
*) git rm -q "$FILE" ;;
esac
if [ "$SIZE" -gt "$THRESHOLD_BYTES" ]; then
echo "$FILE कम्प्रेशन के बाद भी $THRESHOLD_KB KB से ऊपर है (क्वालिटी $QUALITY, $((SIZE / 1000)) KB): हाथ से कम करना होगा।"
REMAINING="$REMAINING $FILE"
fi
done <<< "$FILES"
# कम्प्रेशन से मिला फ़ायदा, जॉब के लॉग में दिखता है ("फ़ायदे को मापना" देखें)।
# -s: /tmp/report.json में हर फ़ाइल के लिए एक JSON ऐरे है (ऊपर के लूप में
# हर ">>" पर एक), "add" उन्हें जोड़ने से पहले एक में मिला देता है।
echo "--- कम्प्रेशन से फ़ायदा ---"
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) बाइट (\(.ratio))"' /tmp/report.json
echo "पहले कुल: $(jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json) बाइट"
echo "बाद में कुल: $(jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json) बाइट"
# git diff --quiet को यह नहीं दिखता कि git rm से हटाई गई .png की जगह
# बिना ट्रैक की .jpg आ गई: status --porcelain बिना ट्रैक फ़ाइलें भी देखता है।
if [ -z "$(git status --porcelain)" ]; then
echo "कमिट करने को कुछ नहीं, सभी इमेज पहले से कम्प्रेस्ड थीं।"
exit 0
fi
git config user.name "jpgboost-ci"
git config user.email "ci@example.com"
git add -A
# [skip ci]: इसके बिना यह पुश एक नई merge_request_event पाइपलाइन चला देता है,
# जो एक और सुधार कमिट पुश करती है, और यह सिलसिला चलता रहता है (अनंत लूप)।
git commit -m "बदली गई इमेज jpgboost-cli से कम्प्रेस करें [skip ci]"
# PUSH_TOKEN एक personal access token है (स्कोप write_repository), जो मास्क
# किए गए CI/CD वेरिएबल के रूप में रखा है: डिफ़ॉल्ट CI_JOB_TOKEN सुरक्षित
# ब्रांच पर पुश करने के लिए काफ़ी नहीं है।
git remote set-url origin "https://gitlab-ci-token:${PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
git push origin "HEAD:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"
# सबसे बेहतर संभव नतीजा हर हाल में पुश होता है (कुछ न होने से बेहतर), पर
# अगर कम से कम एक फ़ाइल न्यूनतम क्वालिटी पर भी सीमा से ऊपर रह जाए तो जॉब
# विफल होता है, ताकि यह चुपचाप स्वीकार होने के बजाय दिखता रहे।
if [ -n "$REMAINING" ]; then
echo "कम्प्रेशन के बावजूद अब भी बहुत भारी फ़ाइलें:$REMAINING"
exit 1
fi
जाँच मोड या सुधार मोड
पिछले हिस्से के दोनों जॉब jpgboost-cli को GitLab की मर्ज रिक्वेस्ट से जोड़ने के दो विपरीत तरीक़े दिखाते हैं। एक रोकता है, दूसरा योगदानकर्ता की जगह ख़ुद ठीक कर देता है।
जाँच मोड में check-image-weight जॉब कुछ नहीं बदलता। यह हर बदली गई इमेज को एक अस्थायी फ़ोल्डर में कम्प्रेस करता है, नतीजे के आकार की तुलना चुनी गई सीमा से करता है, और सीमा पार होने पर CI को विफल कर देता है। रिपॉज़िटरी जस की तस रहती है। इमेज को दोबारा तैयार करके फिर से कमिट करना योगदानकर्ता का काम है; पाइपलाइन बस तब तक मर्ज होने से रोकती है।
सुधार मोड में fix-image-weight जॉब एक क़दम आगे जाता है। यह हर इमेज को रिपॉज़िटरी में उसी जगह सीधे कम्प्रेस करता है, फिर नतीजे को कमिट करके मर्ज रिक्वेस्ट की ब्रांच पर पुश कर देता है। योगदानकर्ता को दोबारा कुछ नहीं करना पड़ता, पर Git हिस्ट्री में एक ऐसा कमिट जुड़ जाता है जो उसने ख़ुद नहीं लिखा।
| कसौटी | जाँच मोड | सुधार मोड |
|---|---|---|
| लक्ष्य | इमेज सीमा पार करे तो CI रोक देना | कम्प्रेस करके मर्ज रिक्वेस्ट में कमिट पुश करना |
| तरीक़ा | --json + jq, चुनी गई सीमा से तुलना, फिर स्पष्ट exit 1 (सीमा के लिए कोई बिल्ट-इन विकल्प नहीं) | सीधा कम्प्रेशन, उसी जगह लिखना, फिर पुश |
| रिपॉज़िटरी पर असर | कोई नहीं, जॉब सिर्फ़ देखता है | मर्ज रिक्वेस्ट में एक अपने आप बना कमिट जुड़ता है |
| योगदानकर्ता पर असर | ख़ुद ठीक करके दोबारा कमिट करना पड़ता है | और कुछ नहीं करना पड़ता |
| ज़रूरी प्रमाणीकरण | कोई नहीं | मास्क किए गए वेरिएबल में project access token या deploy token |
| क़ीमत | रुकावट, हाथ से सुधार का काम योगदानकर्ता पर रहता है | Git हिस्ट्री अपने आप बदलती है, सुरक्षित ब्रांच से टकराव का ख़तरा |
व्यवहार में, जाँच मोड उस टीम के लिए ठीक है जो अपनी इमेज पर संपादकीय नियंत्रण (क्रॉप, रीटच, फ़ॉर्मेट का चुनाव) तब तक अपने पास रखना चाहती है जब तक वे कम्प्रेस्ड रूप में रिपॉज़िटरी में न पहुँच जाएँ। सुधार मोड उस टीम के लिए ठीक है जो इस बारे में कभी सोचना ही नहीं चाहती, भले इसकी क़ीमत हिस्ट्री में एक और अपने आप बना कमिट और सँभालने के लिए एक पुश टोकन हो।
जॉब को बेहतर बनाना
दोनों जॉब पहले ही git diff --name-only --diff-filter=ACM के ज़रिए काम को सिर्फ़ सचमुच बदली गई फ़ाइलों तक सीमित रखते हैं, हर पाइपलाइन पर पूरी रिपॉज़िटरी दोबारा खंगालने के बजाय। जिस रिपॉज़िटरी में सैकड़ों इमेज जमा हों, वहाँ चलने के समय का फ़र्क़ काफ़ी बड़ा होता है।
स्थायी सेल्फ़-होस्टेड रनर के साथ कैशिंग का मतलब बदल जाता है। GitLab CI का cache: निर्देश मुख्य रूप से ऐसे अस्थायी रनर पर निर्भरताएँ वापस लाने के लिए है जो हर जॉब पर शून्य से शुरू होता है। यहाँ JPGBoost.app रनर पर पहले से इंस्टॉल है और उसे दोबारा डाउनलोड करने की ज़रूरत नहीं। cache: ब्लॉक जोड़ने से रनर के लोकल स्टोरेज से मिलने वाली चीज़ के अलावा कुछ हासिल नहीं होगा।
--json विकल्प वह जानकारी देता है जो एक idempotence तंत्र बनाने के लिए चाहिए। compressedSizeBytes, या उससे बेहतर फ़ाइल का हैश, एक रन से दूसरे रन तक सहेज कर रखा जाए तो न बदली फ़ाइलों को पहचाना जा सकता है और हर पाइपलाइन पर उन्हें दोबारा कम्प्रेस करने से बचा जा सकता है।
समांतर प्रोसेसिंग --jobs के ज़रिए मूल रूप से उपलब्ध है। कई फ़ाइलें एक साथ प्रोसेस हो सकती हैं, हर एक अलग-अलग डिकोड होकर मुक्त होती है। इसलिए मेमोरी की खपत मुख्य रूप से --jobs के मान पर निर्भर करती है, बाक़ी बची फ़ाइलों की कुल संख्या पर नहीं। 12 फ़ाइलों के बैच पर दस्तावेज़ --jobs 8 और --jobs 1 के बीच लगभग 4× का फ़ायदा बताते हैं, जो अपने रनर पर यह मान तय करने के लिए उपयोगी अंदाज़ा है, यह ध्यान में रखते हुए कि AVIF और JPEG XL की एन्कोडिंग JPEG या HEIC के मुक़ाबले CPU पर काफ़ी ज़्यादा भारी पड़ती है।
आम मुश्किलें
पहली मुश्किल आउटपुट फ़ॉर्मेट से जुड़ी है। ऊपर की स्क्रिप्ट जान-बूझकर हर इमेज को, PNG समेत, JPEG में बदल देती है। किसी तस्वीर के लिए JPEG आम तौर पर PNG से बेहतर क्वालिटी-से-आकार अनुपात देता है: इसलिए --format jpeg थोपने से फ़ाइल वहाँ भी आकार की सीमा के नीचे आ सकती है जहाँ सिर्फ़ क्वालिटी घटाना काफ़ी न होता। इसकी क़ीमत है पारदर्शिता का चले जाना, क्योंकि JPEG में अल्फ़ा चैनल नहीं होता, और ज़रूरी नहीं कि लॉग में कोई त्रुटि दिखे भी। यह चुनाव तस्वीरों के लिए काम करता है, पर पारदर्शी पृष्ठभूमि वाले PNG लोगो या आइकॉन को चुपचाप बिगाड़ सकता है। जिस रिपॉज़िटरी में दोनों तरह के इस्तेमाल मिले-जुले हों, वहाँ सभी इमेज पर एक ही फ़ॉर्मेट थोपने के बजाय जॉब का दायरा सीमित करना (जैसे किसी अलग फ़ोल्डर तक) बेहतर है।
GitLab CI में किसी स्टेज का जॉब डिफ़ॉल्ट रूप से तभी चलता है जब पिछले स्टेज के सभी जॉब सफल हुए हों। पर fix-image-weight का मतलब तभी है जब check-image-weight सीमा पार होना पकड़े, और ठीक यही वह स्थिति है जिसमें उस डिफ़ॉल्ट व्यवहार के चलते वह कभी चलेगा ही नहीं। needs: [] सुधार वाले जॉब को इस अंतर्निहित निर्भरता से मुक्त कर देता है और उसे जाँच वाले जॉब के समांतर, स्वतंत्र रूप से चलने देता है।
इसके बाद सुधार वाला जॉब उसी ब्रांच पर कमिट पुश करता है जिसने पाइपलाइन चलाई थी। सावधानी न बरती जाए तो वह पुश एक नई मर्ज रिक्वेस्ट पाइपलाइन चला सकता है। अगर नतीजा तब भी बहुत भारी माना जाए, तो जॉब फिर सुधारता है, एक और कमिट पुश करता है, और एक और पाइपलाइन चला देता है। यह कुछ ही मिनटों में दर्जनों कमिट और पाइपलाइन बनाने वाले लूप में बदल सकता है। अपने आप बने कमिट के संदेश में [skip ci] जोड़ने से GitLab को पता चल जाता है कि उस कमिट के लिए नई पाइपलाइन न चलाए।
CI_MERGE_REQUEST_DIFF_BASE_SHA के मुक़ाबले चलाया गया git diff यह भी मानकर चलता है कि वह आधार कमिट लोकल पर मौजूद है। पर GitLab डिफ़ॉल्ट रूप से सीमित गहराई तक क्लोन करता है। जिस मर्ज रिक्वेस्ट में बहुत सारे कमिट जमा हो गए हों, या जब लक्ष्य ब्रांच काफ़ी अलग हो चुकी हो, तो खोजा जा रहा कमिट वहाँ हो ही न। तब git diff ऐसे कारण से विफल होता है जिसका इमेज के वज़न से कोई लेना-देना नहीं। GIT_DEPTH: "0" सेट करने से पूरा क्लोन होता है और यह समस्या टल जाती है, बस हर जॉब पर क्लोन लंबा हो जाता है।
एक और मुश्किल बदलावों की पहचान से जुड़ी है। git diff --quiet सिर्फ़ उन्हीं फ़ाइलों के बदलाव पकड़ता है जिन्हें Git पहले से ट्रैक कर रहा है। पर जैसे ही स्क्रिप्ट किसी इमेज को JPEG में बदलकर git rm से मूल फ़ाइल हटाती है, असली बदलाव दो कामों से बनता है: ट्रैक की गई .png का हटना, और एक नई, बिना ट्रैक की .jpg का बनना। तब सादा git diff कुछ भी न बताए, ऐसा हो सकता है। व्यवहार में जॉब अपने लॉग में बिल्कुल सच्चा कम्प्रेशन फ़ायदा दिखा सकता है, फिर भी "कमिट करने को कुछ नहीं" पर पहुँच सकता है और नतीजा कभी पुश ही न करे। git status --porcelain, जो बिना ट्रैक फ़ाइलों को भी देखता है, इस तरह धोखा नहीं खाता।
Git हिस्ट्री में पहले से मौजूद इमेज भी अपने आप में आकार की एक अलग समस्या हैं। नए कमिट में उन्हें कम्प्रेस्ड संस्करण से बदल देने पर भी पुराने संस्करण Git blob के रूप में हिस्ट्री में बने रहते हैं। रिपॉज़िटरी उन फ़ाइलों की जगह अपने आप वापस नहीं लेती। सिर्फ़ हिस्ट्री दोबारा लिखने से ही वह पुराना डेटा हटेगा, और यह एक विनाशकारी काम है जिसकी किसी ऑटोमेटेड पाइपलाइन में जगह नहीं।
सुधार मोड का अपने आप बना कमिट रिपॉज़िटरी के अपने नियमों से भी टकरा सकता है। कोई सुरक्षित ब्रांच सीधे पुश करने से मना कर सकती है या मर्ज से पहले समीक्षा माँग सकती है। इसी तरह कोई हुक या फ़ॉर्मेटिंग का दूसरा तंत्र उन्हीं फ़ाइलों को छू सकता है और कम्प्रेशन जॉब से टकरा सकता है। इन टकरावों को यह मानकर छोड़ देने के बजाय कि वे बिना रगड़ के साथ रह लेंगे, स्पष्ट रूप से जाँचना चाहिए।
आख़िर में, यह पाइपलाइन सिर्फ़ उन्हीं इमेज को कवर करती है जो रिपॉज़िटरी से होकर गुज़रती हैं, और ख़ासकर उसी मर्ज रिक्वेस्ट प्रवाह से। जो डिज़ाइनर इमेज सीधे किसी CMS, साझा फ़ोल्डर या Git के बाहर के किसी और एसेट स्टोर में डाल देता है, वह इस जाँच को पूरी तरह छोड़ देता है। पाइपलाइन रिपॉज़िटरी में वर्ज़न की गई इमेज की गुणवत्ता सुधारती है, पर अकेले वह एसेट प्रबंधन की पूरी नीति नहीं है।
फ़ायदे को मापना
fix-image-weight यह रिपोर्ट कम्प्रेशन लूप के ठीक बाद अपने ही लॉग में पहले से छापता है (ऊपर "GitLab CI के साथ एकीकरण" वाला हिस्सा देखें), जो मर्ज रिक्वेस्ट के Pipelines टैब से दिखती है, और इसमें कुछ जोड़ने की ज़रूरत नहीं। बारीक़ी यह है: --json आउटपुट का ratio फ़ील्ड originalSizeBytes और compressedSizeBytes के साथ-साथ हर फ़ाइल का कमी प्रतिशत सीधे बता देता है।
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) बाइट (\(.ratio))"' /tmp/report.json
कुल जोड़ के लिए सही कमांड इस पर निर्भर करती है कि जॉब /tmp/report.json कैसे लिखता है। fix-image-weight अपने लूप में हर फ़ाइल के लिए एक JSON ऐरे लिखता है (>>), इसलिए आख़िरी फ़ाइल में एक के बजाय कई ऐरे जुड़े होते हैं। तब सब कुछ पढ़ने के लिए -s (slurp) चाहिए, पर वह उन ऐरे को एक और ऐरे में लपेट देता है: पहले add से उन्हें समतल किए बिना map वहीं विफल हो जाता है (jq: error: Cannot index array with string)। ऊपर की स्क्रिप्ट में यही रूप इस्तेमाल हुआ है।
# पूरे बैच पर कम्प्रेशन से पहले और बाद का कुल
# (कई कॉल में लिखी गई फ़ाइल: fix-image-weight)
jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json
jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json
अगर आप अपनी रिपोर्ट कहीं और बना रहे हैं, लोकल पर ("jpgboost-cli इंस्टॉल करें" देखें) या check-image-weight में, जो सारी फ़ाइलें एक ही कॉल में कम्प्रेस करता है (>), तो वहाँ /tmp/report.json पहले से ही एक अकेला JSON ऐरे है, और तब -s न ज़रूरी है न सही: वह पहले से पूरे ऐरे को दोबारा लपेटकर उल्टी वजह से वही त्रुटि पैदा करता है।
# वही गणना, एक ही कॉल में लिखी गई रिपोर्ट पर
# (लोकल जाँच, या check-image-weight)
jq 'map(.originalSizeBytes) | add' /tmp/report.json
jq 'map(.compressedSizeBytes) | add' /tmp/report.json
jpgboost-cli सिर्फ़ वही मापता है जो वह ख़ुद पैदा करता है: पहले और बाद का आकार। परफ़ॉर्मेंस स्कोर (LCP, Lighthouse स्कोर) अलग से, किसी समर्पित टूल से लेने होते हैं। इस टूल में कुछ भी उसकी गणना ख़ुद नहीं करता, और उसे कम्प्रेशन प्रतिशत से बनावटी ढंग से जोड़ना एक अपुष्ट अनुमान भर होगा।