גירסאות סמנטיות 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)״, ״תהא (SHELL)״, ״לא (SHELL 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 תווים היא כנראה מוגזמת. כמו כן, מערכות ספצפיות עשויות להטיל מגבלות משלהם על גודל המחוזרת.

אודות

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

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

רישיון

Creative Commons - CC BY 3.0