גירסאות סמנטיות 2.0.0

סיכום

בהינתן מספר גרסה MAJOR.MINOR.PATCH, העלה/י את:

  1. גרסה ראשית (MAJOR) כאשר את/ה עושה שינויים שאינם עולים בקנה אחד עם ה API,
  2. גרסה משנית (MINOR) כשאר את/ה מוסיף/ה פונקציונאליות בצורה שמותאמת אחורה, ו
  3. גרסת תיקון (PATCH) כאשר את/ה מבצע/ת תיקוני באגים שמותאמים אחורה.

תוויות נוספות עבור גרסאות קדם הפצה ובניית מטה זמינות כתוספת עבור הפורמט MAJOR.MINOR.PATCH.

מבוא

בעולם של ניהול תוכנה קיים מקום מפחיד בשם “גיהנום התלויות (dependency hell)״. ככל שהמערכת גדלה ואת/ה משלב/ת יותר ויותר חבילות לתוכנה, קיים סיכוי גבוהה יותר שתמצא/י את עצמך, יום אחד, בבור של יאוש.

בתוכנות עם המון תלויות, שחרור גרסה של חבילה חדשה יכול במהירות להפוך לסיוט. אם מפרט התלויות הדוק מידי, את/ה בסכנה של ״נעילת גרסה (version lock)״ (חוסר היכולת לשדרג חבילה מבלי לשחרר גרסה חדשה של כל חבילה תלויה). אם מפרט התלויות רופף מידי, באופן בלתי נמנע את/ה תכווה/י ע״י ״מתירנות גרסה (version promiscuity)״ (השערה של תאימות, יותר מהסביר, עם גרסאות עתידיות). ״גיהנום התלויות״ הוא כאשר את/ה נמצא/ת במקום בו נעילת גרסה ו/או מתירנות גרסה מונעים ממך בקלות ובבטחה להניע את הפרויקט שלך קדימה.

על מנת לפתור בעיה זו לבעיה זו, אני מציע סט פשוט של כללים ודרישות המכתיבים כיצד מספרי גרסאות יוקצו ויתווספו. כללים אלה מבוססים על מנהגים משותפים, קיימים ונפוצים בפרויקטי קוד פתוח וסגור. על מנת שמערכת זו תפעל, תחילה צריך להכריז על API ציבורי. זה אולי יהיה מורכב מתיעוד או יאכף על ידי הקוד עצמו. בכל מקרה, זה חשוב שה API יהיה ברור ומדויק. ברגע שאת/ה מגדיר/ה את ה API הציבורי שלך, את/ה מתקשר/ת בו את השינויים עם תוספות ספצפיות למספר הגרסה. הסתכל/י על תבנית גרסה של X.Y.Z (בפורמט Major.Minor.Patch). תיקוני באגים שלא משפיעים על ה API יגדילו את מספר את מספר התיקון (Patch), תוספות / שינויים תואמי API לאחור יגדילו את הגרסה המשנית (Minor), ושינויים שאינם נתמכים לאחור ב API יגדילו את הגרסה הראשית (Major).

אני קורא למערכת ״גירסאות סמנטיות”. במסגרת זו, מספרי גרסאות והדרך שבה הם משתנים מעבירה משמעות על הקוד ועל מה שהשתנה מגרסה אחת לאחרת.

מפרט גירסאות סמנטיות (SemVer)

מילות המפתח ״חייב (MUST)״, ״אסור (MUST NOT)״, “חובה (REQUIRED)״, ״תהא (SHALL)״, ״לא (SHALL NOT)״, ״צריך (SHOULD)״, ״לא צריך (SHOULD NOT)״, ״מומלץ (RECOMMENDED)״, ״יכול (MAY)״, ״אפשרי (OPTIONAL)״ אשר מופיעות במסמך, צריכות להיות מפורשות כמתואר במסמך RFC 2119.

  1. תוכנה באמצעות גירסאות סמנטיות נדרשת להכריז על API ציבורי. ניתן להכריז על API זה בקוד עצמו או אך ורק בתיעוד. עם זאת זה צריך להיעשות מדויק ומקיף.

  2. מספר גרסה רגילה חייב (MUST) להיות בצורה X.Y.Z כאשר Y, X, ו Z הם מספרים שלמים (Integers) שאינם שליליים, ואסור (Must Not) שיכילו אפסים מובילים. X הוא גרסה ראשית (Major), לאחריו Y הוא גרסה משנית (Minor), ולבסוף Z הוא גרסת תיקון (Patch). כל רכיב במספר הגרסה חייב (MUST) להגדיל את המספר נומרית. לדוגמה: 1.9.0 -> 1.10.0 -> 1.11.0.

  3. לאחר שחבילה שוחררה, אסור (MUST NOT) לשנות את התוכן של החבילה באותה גרסה. כל שינוי חייב (MUST) להשתחרר כגרסה חדשה.

  4. גרסה ראשית אפס (0.z.y) היא עבור פיתוח ראשוני. בהינתן שכל דבר יכול להשתנות בכל זמן, ה API הציבורי לא צריך להיחשב יציב.

  5. גרסה 1.0.0 מגדירה את ה API הציבורי. הדרך שבה מספר הגרסה עולה לאחר שחרור גרסה זו תלוי ב-API הציבורי ובאופן בו הוא משתנה.

  6. גרסת תיקון (Patch) במיקום Z (כלומר x > 0 Z.y.x) חייבת (MUST) לעלות אם ורק אם הוצגו תיקוני באגים תואמים לאחור. תיקון באג שתואם לאחור מוגדר כשינוי פנימי שמתקן התנהגות לא נכונה.
  7. גרסה המשנית (Minor) במיקום Y (כלומר x > 0 z.Y.x) חייבת (MUST) לעלות שינויי פונקציונאליות חדש, תואם אחורה, הוצג ב API הציבורי. הוא חייב (MUST) לעלות אם פונקציונאליות כלשהי ב API הציבורי מסומן כ deprecated. הוא יכול (MAY) לעלות אם הוצגו שינויים או חידושיים משמעותיים בתוך הקוד הפרטי. הוא יכול (MAY) לכלול שינויים ברמת תיקון (Patch). מספר הגרסה ברמת תיקון (Patch) חייב (MUST) להיות מאופס ל-0 כאשר מספר הגרסה המשנית (Minor) עולה.
  8. גרסה ראשית (Major) במיקום X (כלומר X > 0 z.y.X) חייבת (MUST) לעלות אם הוצגו שינויים כלשהם ל API הציבורי שאינם תואמים אחורה. היא יכולה (MAY) לכלול שינויים ברמת משנית (Minor) וברמת תיקון (Patch). מספרי גרסאות ברמת תיקון (Patch) וברמה משנית (Minor) חייבים (MUST) להיות מאופסים ל 0 כאשר עולים מספר גרסה ראשית (Major).
  9. גרסת קדם הפצה יכולה (MAY) להיות מסומנת על ידי מקף ומזהים מופרדים בנקודות מיד לאחר גרסת התיקון (Patch). מזהים חייבים (MUST) להיות מורכבים מאותיות, מקף, ומספרים בתקן ASCII בלבד [0-9A-Za-z-]. אסור שהמזהה יהיה ריק. אסור שמזהים מספריים יכילו אפסים מובילים. לגרסאות קדם הפצה יש זכות נמוכה מלגרסאות רגילות. גרסת קדם הפצה מציינת כי הגרסה אינה יציבה ויכול להיות שלא תספק את הדרישות כמו גרסה רגילה. דוגמאות:
    1.0.0-alpha,  1.0.0-alpha.1,   1.0.0-0.3.7,   1.0.0-x.7.z.92

  10. גרסה בעלת מטה נתונים (metadata build) יכולה (MAY) להיות מסומנת על ידי סימן פלוס ומזהים המפורדים בנקודות הבאים מיד לאחר גרסת התיקון (Patch) או גרסת קדם הפצה. מזהים חייבים (MUST) להיות מורכבים מאותיות, מקף, ומספרים בתקן ASCII בלבד [0-9A-Za-z-]. אסור שהמזהה יהיה ריק. בעת קביעת עדיפות גרסה צריך (SHOULD) להתעלם מגרסת מטה נתונים. לשתי גרסאות שההבדל היחיד בינהן הוא גרסת המטה נתונים יש אותה עדיפות. דוגמאות:
    1.0.0-alpha+001,  1.0.0+20130313144700,   1.0.0-beta+exp.sha.5114f85

  11. עדיפות מתייחסת לאופן בו משווים גרסאות זה לזה כאשר הם מסודרים. עדיפות חייבת (MUST) להיות מחושבת על ידי הפרדת הגרסה לראשי (Major), משני (Minor), תיקון (Patch) ומזהי קדם הפצה בסדר הזה (נתוני מטה אינם נלקחים בחשבון בעדיפות). העדיפות נקבעת על ידי ההבדל הראשון כאשר משווים את כל המזהים מספרית משמאל לימין כדלקמן: ראשי (Major), משני (Minor) ותיקון (Patch) תמיד משווים מספרית. דוגמה 2.1.1 > 2.1.0 > 2.0.0 > 1.0.0. כאשר ראשי (Major), משני (Minor) ותיקון (Patch) שווים, ולגרסת קדם הפצה יש עדיפות נמוכה יותר מאשר לגרסה רגילה. דוגמה ֿ1.0.0 > 1.0.0-alpha. עדיפות עבור שתי גרסאות קדם הפצה עם גרסאות ראשי (Major), משני (Minor), תיקון (Patch) זהים חייב (MUST) להיקבע על ידי השוואה של מזהה בין הנקודות משמאל לימין עד שנמצא הבדל כדלקמן: מזהים המורכבים מספרות בלבד משווים בצורה מספרית (נומרית) ומזהים עם אותיות או מקפים משווים לקסיקלית בסדר מיון של תקן ASCII. למזהים מספריים תמיד תיהיה עדיפות נמוכה ממזהים שאינם מספריים. לקבוצה גדולה יותר של מזהים בגרסת קדם הפצה יש עדיפות גבוהה יותר מקבוצה קטנה יותר, אם כל אמצעי ההשוואה למזהים שקדמו שווים. דוגמאות:
    1.0.0 > 1.0.0-rc.1 > 1.0.0-beta.11 > 1.0.0-beta.2 > 1.0.0-beta > 1.0.0-alpha.beta > 1.0.0-alpha.1 > 1.0.0-alpha

למה להשתמש בגרסאות סמנטיות?

זה לא רעיון חדש או מהפכני. למעשה, כנראה שאת/ה עושה משהו קרוב לזה כבר עכשיו. הבעיה היא ש״קרוב״ זה לא מספיק טוב. ללא תאימות כלשהי למפרט פורמלי, מספרי גרסה הם למעשה חסרי תועלת לניהול תלויות. על ידי מתן שם ברור והגדרה ברורה לרעיונות שהוצגו כאן, תקשור הכוונות שלנו למשתמשי/ות התוכנה נעשה קל יותר. לאחר שהכוונות האלו ברורות, גמישות (אבל לא גמישות מידי), יכול בסופו של דבר להתבצע מפרט תלויות.

דוגמא פשוטה תדגים כיצד גירסאות סמנטיות יכולות להפוך את ״גינהום התלויות״ לנחלת העבר. דמיינו ספריה בשם ״כבאית״. שדורשת ספרייה בשם ״סולם״ שעומדת במפרט של גרסאות סמנטיות. באותו הזמן שבו הכבאית נוצרה, ספריית סולם הייתה בגרסה 3.1.0. מכיוון שהכבאית משתמשמת בפונקציונליות שהוצגה בגרסה 3.1.0, ניתן לציין את התלות של ספריית כבאית בספריית סולם כגדול או שווה לגרסה מספר 3.1.0 אך פחות מ 4.0.0. עכשיו כאשר גרסת סולם מספר 3.1.1 ומספר 3.2.0 מתפרסמות, אפשר להשתמש בהן בספריית הכבאית בבטחה, ולדעת שהן תהיינה תואמות את ה API הקיים של הספרייה עליו אנו מסתמכים בספריית הכבאית שלנו.

כמפתח/ת אחראי/ת תוכל/י, כמובן, לוודא כי כל שדרוגי החבילה מתפקדים כפי שפורסם. העולם האמיתי הינו מקום מבולגן; אין שום דבר שאנחנו יכולים לעשות לגבי זה למעט להיות על המשמר. מה שאת/ה יכול/ה לעשות זה לתת למפרט הגירסאות הסמנטיות לספק לך דרך שפויה לשחרר ולשדרג חבילות מבלי לגלגל גרסה חדשה של חבילות תלויות, מה שחוסך לך זמן וטרחה.

אם כל זה נשמע טוב ונחשק, כל מה שאת/ה צריך/ה לעשות כדי להתחיל להשתמש בגרסאות סמנטיות הוא להכריז שאת/ה עושה כך, ולאחר מכן לבצע את הכללים. כלול קישור לאתר זה בקובץ ה README שלך כדי שאחרים ידעו את הכללים ויוכלו להפיק מהם תועלת.

שאלות נפוצות

כיצד עלי להתמודד עם תיקונים בשלב הפיתוח הראשוני O.y.z?

הדבר הפשוט ביותר לעשות הוא להתחיל לשחרר את הפיתוח הראשוני שלך בגרסה 0.1.0 ואז להגדיל את הגרסה המשנית (Minor) לכל גרסה שבא אחריה.

איך אדע מתי אוכל לשחרר את גרסה 1.0.0?

אם התוכנה שלך נמצאת בשימוש בשימוש לקוחות אמיתיים (Production), את/ה כנראה צריך/ה להיות כבר בגרסה 1.0.0. אם יש לך API יציב שעליו המשתמש יכול לסמוך, את/ה צריך/ה להיות על גרסה 1.0.0. אם את/ה דואג/ת הרבה לגבי תאימות לאחור, את/ה בוודאי כבר צריך/ה להיות על גרסה 1.0.0.

האם זה לא מונע פיתוח מהיר ואיטרציות מהירות?

כל העניין של גרסה ראשית (Major) אפס הוא פיתוח מהיר. אם את/ה משנה את ה API כל יום את/ה צריך/ה להיות על גרסה 0.z.y או על ענף פיתוח נפרד בשביל הגרסה ראשית (Major) הבאה.

אם אפילו השינויים הקטנים ביותר ל API ששוברים תמיכה לאחור מחייבים לעלות גרסה ראשית, האם אני לא אסיים עם גרסה 42.0.0 מהר מאוד?

זוהי שאלה של פיתוח אחראי וראיית הנולד. שינויים ששוברים תמיכה צריכים לקרות בצורה קלילה לתוכנה בעלת הרבה תלויות. העלות שנגרמת משדרוג יכולה להיות משמעותית. מכיוון שאת/ה חייב/ת לעלות גרסה ראשית (Major) כדי לשחרר שינויים שוברי תאימות, את/ה תחשוב/תחשבי היטב על ההשפעה של השינויים שלך, ותעריך/י את יחס העלות / תועלת המעורב בכך.

תיעוד כל ה API הציבורי זה יותר מדי עבודה!

זוהי האחריות שלך בתור מפתח/ת מקצועי/ת לתעד את התוכנה שמיועדת לשימוש בידי אחרים. ניהול המורכבות של התכונה הינו חשוב מאין כמוהו לשמירת הפרויקט יעיל, וזה משהו שקשה לעשות אם אף אחד לא יודע כיצד להשתמש בתוכנה שלך, או לאיזה מתודות אפשר לקרוא בבטחה. בטווח הארוך, גרסאות סמנטיות, וההתעקשות על API מוגדר היטב יכול לשמור על כולם ועל הכל פועל בצורה חלקה.

מה עלי לעשות אם אני בטעות משחרר שינויים לא תואמים אחורה כגרסה משנית (Minor)?

ברגע שאת/ה מבין/ה ששברת את מפרט הגירסאות הסמנטיות, תקן/י את הבעיה ושחרר/י גרסה משנית (Minor) חדשה עם שיחזור התאימות לאחור. אפילו תחת הנסיבות האלו זה בלתי מקובל לשנות גרסאות קודמות. אם זה מתאים למקרה, תעד/י את הגרסה הסוררת ויידע/י את המשתמשים שלך על הבעיה כדי שיהיו מודעים לגרסה זו.

מה עלי לעשות אם אני מעדכן את התלויות שלי מבלי לשנות את ה API הציבורי?

זה יחשב כתואם מכיוון שזה לא משפיע על ה API הציבורי. תוכנה שתלויה באופן מפורש באותן תלויות בהן התוכנה שלך תלויה צריכה לכלול מפרט תלויות משלה והמחבר/ת של אותה התוכנה י/תבחין בקונפליקטים. קביעה האם מדובר בשינוי ברמת תיקון (Patch) או ברמה משנית (Minor) תלוי בהאם את/ה מעדכן/ת את התלויות שלך כדי לתקן באג או להציג פונקציונליות חדשה. בדרך כלל הייתי מצפה לקוד נוסף במקרה האחרון, ובמקרה כזה זה כמובן מצריך העלה של גרסה משנית (Minor).

מה אם אני משנה בטעות את ה API הציבורי באופן שאינו תואם עם שינוי מספר הגרסה (למשל קוד מציג שינוי שובר תאימות ראשי (Major) בשחרור בעל שינוי בגרסת התיקון (Patch) בלבד)

השתמש/י בשיקול הדעת שלך. אם יש לך קהל ענק שיושפע באופן גדול משינוי ההתנהגות אחורה לכוונה המקורית של ה API הציבורי, אז יכול להיות שהכי טוב יהיה לבצע שחרור של גרסה ראשית (Major), אפילו אם התיקון יכול להיחשב כגרסת תיקון (Patch) בלבד. זכור/זכרי, ניהול גרסאות סמנטי הוא על העברת מסר באיך שמספר הגרסה משתנה. אם השינויים האלה חשובים למשתמשים שלך, השתמש/י במספר הגרסה כדי ליידע אותם.

איך אני צריך לטפל ב deprecating ?

פונקציונליות שהופכות ל deprecated הינם תהליך נורמלי של פיתוח תוכנה ולפעמים אף נדרש כדי להתקדם. כאשר את/ה הופך/ת חלק מה API הציבורי שלך ל deprecated, את/ה צריך/ה לעשות שני דברים: (1) לעדכן את את התיעוד שלך כדי לתת למשתמשים לדעת על השינוי, (2) לשחרר גרסה משנית (Minor) עם הפונקציונליות שבמצב deprecated. לפי שאת/ה מסיר/ה לחלוטין את הפונקציונליות הזאת בגרסה ראשית (Major) חדשה צריך להיות לפחות גרסה אחת משנית (Minor) שמכילה את הפונקציונליות עם הודעת ה deprecated כדי שהמשתמשים יוכלו לעבור בקלות ל API החדש.

האם ל semver יש מגבלת גודל על ה String של הגרסה?

לא, אבל הפעל/י שיקול דעת. לדוגמה, מחרוזת גרסה בעלת 255 תווים היא כנראה מוגזמת. כמו כן, מערכות ספצפיות עשויות להטיל מגבלות משלהם על גודל המחוזרת.

האם “v1.2.3” גרסה סמנטית?

לא, “v1.2.3” אינו גרסה סמנטית. עם זאת, קידומת “v” לגרסה סמנטית היא דרך נפוצה (באנגלית) לציין מספר גרסה. לעתים קיצור המילה “גרסה” כ-“v” מופיע בניהול גרסאות.

האם יש ביטוי רגולרי מומלץ (RegEx) לבדיקת גרסה סמנטית?

יש שניים. אחד עם קבוצות שמות (מערכות תומכות: PCRE, Perl, PHP, R, Python ו-Go).

כאן: https://regex101.com/r/Ly7O1x/3

ואחד עם קבוצות לכידה ממוספרות (תואם ל-ECMA Script (JavaScript), PCRE, Perl, PHP, R, Python ו-Go)

כאן: https://regex101.com/r/vkijKf/1

אודות

מפרט הגירסאות הסמנטיות נכתב על ידי טום פרסון-ורנר ממציא ה Gravatars וממייסדי GitHub.

אם את/ה רוצה להשאיר משוב, אנא פתח/י issue ב GitHub.

רישיון

Creative Commons ― CC BY 3.0