גרסאות סמנטיות 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. לאחר שחבילה שוחררה, אסור לשנות את התוכן של החבילה באותה גרסה. כל שינוי חייב להשתחרר כגרסה חדשה.

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

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

  6. גרסת תיקון (Patch) במיקום Z (כלומר x > 0 Z.y.x) חייבת לעלות אם ורק אם הוצגו תיקוני באגים תואמים לאחור. תיקון באג שתואם לאחור מוגדר כשינוי פנימי שמתקן התנהגות לא נכונה.
  7. גרסה משנית (Minor) במיקום Y (כלומר x > 0 z.Y.x) חייבת לעלות שינויי פונקציונאליות חדש, תואם אחורה, שהוצג ב 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” לגרסה סמנטית היא דרך נפוצה (באנגלית) לציין מספר גרסה. לעתים קיצור המילה “גרסה” (version)כ-“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