לדלג לתוכן

מוסרים גרסה שאפשר לחקור

הקוד עבר בדיקות במחשב המפתח. עכשיו נבקש ממחשב אחר לבנות, להתקין, להריץ ולחקור אותו. כל הנחה שלא כתבנו תופיע בדרך כלל כאן: כלי שלא ננעל לגרסה, DLL שנמצא במקרה בPATH או PDB שכבר אינו מתאים לexe.

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

זהות של source אינה זהות של binary

אותו commit יכול להיבנות עם קומפיילרים, flags ותלויות שונים. עץ עבודה עם שינויים מקומיים אינו מתואר במלואו על ידי hash של commit. לכן build ID יקשר source revision, מצב עץ העבודה, toolchain, תלויות ותוצרי הבנייה.

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

מייצרים manifest של תוצרים

הסקריפט הבא, inventory.ps1, מיועד לתיקיית staging של release. הוא עובר רק בתוכה, דוחה reparse points, מסרב לכלול סוגי קבצים שעלולים להכיל מפתחות ומייצר manifest חדש מחוץ לתיקייה. הסירוב אינו מוכיח שאין secrets בקבצים אחרים; הוא מגלה כמה טעויות הפצה נפוצות.

param(
    [Parameter(Mandatory)][string]$ReleaseDirectory,
    [Parameter(Mandatory)][string]$BuildId,
    [Parameter(Mandatory)][string]$OutputFile
)
$ErrorActionPreference = 'Stop'
$releaseRoot = (Resolve-Path -LiteralPath $ReleaseDirectory).Path
if (-not (Test-Path -LiteralPath $releaseRoot -PathType Container)) {
    throw 'ReleaseDirectory must be a directory'
}
$outputPath = [System.IO.Path]::GetFullPath($OutputFile)
$prefix = $releaseRoot.TrimEnd('\') + '\'
if ($outputPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
    throw 'Manifest must be outside the directory being inventoried'
}
$pending = [System.Collections.Generic.Stack[string]]::new()
$pending.Push($releaseRoot)
$records = [System.Collections.Generic.List[object]]::new()
while ($pending.Count -gt 0) {
    $directory = $pending.Pop()
    foreach ($entry in Get-ChildItem -LiteralPath $directory -Force) {
        if (($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
            throw ('Reparse point in staging: ' + $entry.FullName)
        }
        if ($entry.PSIsContainer) {
            $pending.Push($entry.FullName)
            continue
        }
        if ($entry.Extension.ToLowerInvariant() -in '.pfx', '.key', '.pem') {
            throw ('Review excluded key-like file: ' + $entry.Name)
        }
        $hash = (Get-FileHash -LiteralPath $entry.FullName -Algorithm SHA256).Hash
        $records.Add([pscustomobject]@{
            path = $entry.FullName.Substring($prefix.Length).Replace('\', '/')
            bytes = $entry.Length
            sha256 = $hash.ToLowerInvariant()
        })
    }
}
$manifest = [ordered]@{
    format = 1
    build_id = $BuildId
    created_utc = [DateTime]::UtcNow.ToString('o')
    files = @($records | Sort-Object path)
}
$json = $manifest | ConvertTo-Json -Depth 6
$stream = [System.IO.File]::Open($outputPath, [System.IO.FileMode]::CreateNew,
                               [System.IO.FileAccess]::Write)
try {
    $encoding = [System.Text.UTF8Encoding]::new($false)
    $bytes = $encoding.GetBytes($json)
    $stream.Write($bytes, 0, $bytes.Length)
    $stream.Flush()
} finally {
    $stream.Dispose()
}

נפעיל אותו עם build ID שמגיע מהpipeline שלנו ועם שם פלט חדש. ההפרדה מונעת מצב שבו manifest כולל hash של עצמו ומשתנה בכל הרצה.

.\inventory.ps1 -ReleaseDirectory .\release `
    -BuildId 'course-window-1.0.1-build-17' `
    -OutputFile .\release-1.0.1-build-17.json

הסקריפט מניח שתיקיית staging אינה משתנה בזמן האיסוף. אם תהליך אחר מחליף קובץ בין קריאת האורך לחישוב הhash, אין לנו snapshot אטומי. בpipeline נסיים כתיבה, נסגור producerים ונאסוף מתוך staging קפוא לפני חתימה והפצה.

רשימת קבצים אינה רשימת תלויות

הmanifest מתאר מה נמצא בתיקייה. הוא אינו SBOM מלא, משום שאינו מזהה packages, versions, licenses ויחסים בין רכיבים. ספרייה שנקשרה סטטית יכולה להיות בתוך exe בלי להופיע כDLL נפרד. המנגנון של runtime שמגיע מהמערכת או plugin שנטען לפי תצורה יכולים להיות תלות בלי להיכלל בתיקייה.

נפיק SBOM בעזרת כלי שתומך במערכת הבנייה והפורמט שבחרנו, ונבדוק את התוצאה מול dependency inventory ידני. כלי כמו SBOM Tool של Microsoft יכול לעזור ביצירת מסמך מובנה; הוא עדיין צריך קלט מדויק וסריקה של מסלולי התלות שהמוצר באמת משתמש בהם.

נבדוק גם provenance: מאין הגיע package, איך אומתה הגרסה שלו ואיזה lockfile שימש לבנייה. שם ספרייה בקובץ טקסט אינו הוכחה שהבינארי מכיל דווקא אותה גרסה.

בונים בסביבה נקייה

במכונת הבנייה הנקייה נתקין רק את הכלים שהוגדרו, נשלוף source revision ידוע ונבצע build מהתחלה. נבדוק שאין reference לנתיב מוחלט של המפתח ושאין קובץ שנוצר ידנית אך לא נשמר בsource או בpipeline.

תהליך reproducible שואף לתוצרים שניתן לשחזר, אך timestamps, חתימות ופרטים של toolchain יכולים להשפיע על השוואת bytes. נגדיר מה בדיוק משווים: binary לפני חתימה, package חתום או קבוצת שדות מנורמלת. לא נקרא לשתי הרצות "זהות" רק מפני ששתיהן הציגו חלון.

אחרי הבנייה נתקין במכונה נוספת שאינה מכונת build. כך נפריד בין תלות בכלי פיתוח לבין תלות נדרשת בזמן ריצה. נבדוק משתמש מוגבל, תצורה חסרה, נתיב ארוך ומסלול שדרוג מהגרסה הקודמת לפי דרישות המוצר.

מכינים תקלה שלא נכתבה בתיאור

נמסור לחוקר dump או trace עם build ID וקלט, בלי לספר מראש אם הבעיה היא דליפה, lock או קריאה לרשת. הוא יצטרך לנסח השערה, לבחור ראיות ולתקן את הinvariant שהופר.

הבדיקה אינה מבחן זיכרון של פקודות WinDbg. היא בודקת האם אפשר להבחין בין עובדה להשערה: המחסנית מראה המתנה, אבל מי צריך להעיר? האירוע האחרון הוא send, אבל האם השרת כבר ביצע commit? הheap גדול, אבל מי עדיין מחזיק את האובייקטים?

אחרי התיקון נחזור לאותו artifact ולrepro ונוסיף regression test. לא נחליף את כל המערכת בבת אחת, משום שאז יהיה קשה להסביר איזה שינוי פתר את התקלה ואילו שינויים רק הסתירו אותה.

שדרוג שנעצר באמצע

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

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

מה נחשב למסירה

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

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