VFS for Git #729
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: VFS for Git | |
| run-name: ${{ inputs.run_name || 'VFS for Git' }} | |
| on: | |
| pull_request: | |
| branches: [ master, releases/shipped ] | |
| push: | |
| branches: [ master, releases/shipped ] | |
| workflow_dispatch: | |
| inputs: | |
| git_version: | |
| description: 'Microsoft Git version tag to include in the build (leave empty for default)' | |
| required: false | |
| type: string | |
| run_name: | |
| description: 'Optional display name for this run (used for cross-repo automation)' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| actions: read | |
| checks: read | |
| env: | |
| GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.53.0.vfs.0.7' }} | |
| jobs: | |
| validate: | |
| runs-on: windows-2025 | |
| name: Validation | |
| outputs: | |
| skip: ${{ steps.check.outputs.result }} | |
| steps: | |
| - name: Look for prior successful runs | |
| id: check | |
| if: github.event.inputs.git_version == '' | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| result-encoding: string | |
| script: | | |
| /* | |
| * It would be nice if GitHub Actions offered a convenient way to avoid running | |
| * successful workflow runs _again_ for the respective commit (or for a tree-same one): | |
| * We would expect the same outcome in those cases, right? | |
| * | |
| * Let's check for such a scenario: Look for previous runs that have been successful | |
| * and that correspond to the same commit, or at least a tree-same one. If there is | |
| * one, skip running the build and tests _again_. | |
| * | |
| * There are challenges, though: We need to require those _jobs_ to succeed before PRs | |
| * can be merged. You can mark workflow _jobs_ as required on GitHub, but not | |
| * _workflows_. So if those jobs are now simply skipped, the requirement isn't met and | |
| * the PR cannot be merged. We can't just skip the job. Instead, we need to run the job | |
| * _but skip every single step_ so that the job can "succeed". | |
| */ | |
| try { | |
| // Figure out workflow ID, commit and tree | |
| const { data: run } = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| }); | |
| const workflow_id = run.workflow_id; | |
| const head_sha = run.head_sha; | |
| const tree_id = run.head_commit.tree_id; | |
| /* | |
| * If the given workflow run was itself a "skip" run that references another | |
| * run, follow the chain to find the deepest *actual* successful, non-skipped | |
| * run, so we don't end up pointing at a chain of skip-runs. | |
| */ | |
| const SKIP_NOTICE_RE = /^Skipping: There already is a successful run: (.+)$/ | |
| const RUN_URL_RE = /\/actions\/runs\/(\d+)/ | |
| const MAX_CHAIN_LENGTH = 10 | |
| async function resolveSkipChain(initialRunId, initialRunUrl) { | |
| let currentRunId = initialRunId | |
| let currentRunUrl = initialRunUrl | |
| console.log(`Resolving skip chain starting at ${currentRunUrl}`) | |
| for (let i = 0; i < MAX_CHAIN_LENGTH; i++) { | |
| let referencedUrl = null | |
| try { | |
| const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: currentRunId, | |
| }) | |
| const validationJob = jobsData.jobs.find((j) => j.name === 'Validation') | |
| if (!validationJob) { | |
| console.log(`No 'Validation' job found on ${currentRunUrl}; treating as the real run`) | |
| return currentRunUrl | |
| } | |
| const { data: annotations } = await github.rest.checks.listAnnotations({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: validationJob.id, | |
| }) | |
| for (const annotation of annotations) { | |
| const match = annotation.message && annotation.message.match(SKIP_NOTICE_RE) | |
| if (match) { | |
| referencedUrl = match[1] | |
| break | |
| } | |
| } | |
| } catch (e) { | |
| // If we cannot inspect this run, fall back to the URL we already have | |
| console.log(`Failed to inspect ${currentRunUrl}: ${e.message}; stopping chain resolution`) | |
| return currentRunUrl | |
| } | |
| if (!referencedUrl) { | |
| console.log(`${currentRunUrl} is not a skip-run; using it as the resolved run`) | |
| return currentRunUrl | |
| } | |
| console.log(`${currentRunUrl} is a skip-run referencing ${referencedUrl}; following the chain`) | |
| const idMatch = referencedUrl.match(RUN_URL_RE) | |
| if (!idMatch) { | |
| console.log(`Could not parse a run ID from ${referencedUrl}; stopping chain resolution`) | |
| return referencedUrl | |
| } | |
| const referencedId = Number(idMatch[1]) | |
| let referencedRun | |
| try { | |
| const response = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: referencedId, | |
| }) | |
| referencedRun = response.data | |
| } catch (e) { | |
| console.log(`Could not fetch referenced run ${referencedUrl}: ${e.message}; stopping chain resolution`) | |
| return currentRunUrl | |
| } | |
| if (referencedRun.status !== 'completed' || referencedRun.conclusion !== 'success') { | |
| console.log(`Referenced run ${referencedUrl} is no longer usable (status=${referencedRun.status}, conclusion=${referencedRun.conclusion}); stopping chain resolution`) | |
| return currentRunUrl | |
| } | |
| currentRunId = referencedRun.id | |
| currentRunUrl = referencedRun.html_url | |
| } | |
| console.log(`Reached MAX_CHAIN_LENGTH (${MAX_CHAIN_LENGTH}); stopping at ${currentRunUrl}`) | |
| return currentRunUrl | |
| } | |
| // See whether there is a successful run for that commit or tree | |
| const { data: runs } = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 500, | |
| workflow_id, | |
| }); | |
| // first look at commit-same runs, then at finished ones, then at in-progress ones | |
| const rank = (a) => (a.status === 'in_progress' ? 0 : (head_sha === a.head_sha ? 2 : 1)) | |
| const demoteInProgressToEnd = (a, b) => (rank(b) - rank(a)) | |
| for (const run of runs.workflow_runs.sort(demoteInProgressToEnd)) { | |
| if (head_sha !== run.head_sha && tree_id !== run.head_commit?.tree_id) continue | |
| if (context.runId === run.id) continue // do not wait for the current run to finish ;-) | |
| if (run.event === 'workflow_dispatch') continue // skip runs that were started manually: they can override the Git version | |
| if (run.status === 'in_progress') { | |
| // poll until the run is done | |
| const pollIntervalInSeconds = 30 | |
| let seconds = 0 | |
| for (;;) { | |
| console.log(`Found existing, in-progress run at ${run.html_url}; Waiting for it to finish (waited ${seconds} seconds so far)...`) | |
| await new Promise((resolve) => { setTimeout(resolve, pollIntervalInSeconds * 1000) }) | |
| seconds += pollIntervalInSeconds | |
| const { data: polledRun } = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: run.id | |
| }) | |
| if (polledRun.status !== 'in_progress') break | |
| } | |
| } | |
| if (run.status === 'completed' && run.conclusion === 'success') { | |
| console.log(`Found previous successful run at ${run.html_url} (head_sha=${run.head_sha}, tree_id=${run.head_commit?.tree_id})`) | |
| const resolvedUrl = await resolveSkipChain(run.id, run.html_url) | |
| core.notice(`Skipping: There already is a successful run: ${resolvedUrl}`) | |
| return resolvedUrl | |
| } | |
| } | |
| return '' | |
| } catch (e) { | |
| core.error(e) | |
| core.warning(e) | |
| } | |
| - name: Checkout source | |
| if: steps.check.outputs.result == '' | |
| uses: actions/checkout@v6 | |
| - name: Validate Microsoft Git version | |
| if: steps.check.outputs.result == '' | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| & "$env:GITHUB_WORKSPACE\.github\workflows\scripts\validate_release.ps1" ` | |
| -Repository microsoft/git ` | |
| -Tag $env:GIT_VERSION && ` | |
| Write-Host ::notice title=Validation::Using microsoft/git version $env:GIT_VERSION | |
| - name: Download microsoft/git installers | |
| if: steps.check.outputs.result == '' | |
| shell: cmd | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release download %GIT_VERSION% --repo microsoft/git --pattern "Git*.exe" --dir MicrosoftGit | |
| - name: Create Git install script | |
| if: steps.check.outputs.result == '' | |
| shell: cmd | |
| run: | | |
| >MicrosoftGit\install.bat ( | |
| echo @ECHO OFF | |
| echo SETLOCAL | |
| echo. | |
| echo IF "%%PROCESSOR_ARCHITECTURE%%"=="AMD64" ^( | |
| echo SET GIT_ARCH=64-bit | |
| echo ^) ELSE IF "%%PROCESSOR_ARCHITECTURE%%"=="ARM64" ^( | |
| echo SET GIT_ARCH=arm64 | |
| echo ^) ELSE ^( | |
| echo ECHO Unknown architecture: %%PROCESSOR_ARCHITECTURE%% | |
| echo exit 1 | |
| echo ^) | |
| echo. | |
| echo FOR /F "tokens=* USEBACKQ" %%%%F IN ^( `where /R %%~dp0 Git*-%%GIT_ARCH%%.exe` ^) DO SET GIT_INSTALLER=%%%%F | |
| echo. | |
| echo SET LOGDIR=%%~dp0\logs | |
| echo IF EXIST %%LOGDIR%% ^( rmdir /S /Q %%LOGDIR%% ^) | |
| echo mkdir %%LOGDIR%% | |
| echo. | |
| echo ECHO Installing Git ^(%%GIT_ARCH%%^)... | |
| echo %%GIT_INSTALLER%% /LOG="%%LOGDIR%%\git.log" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLOWDOWNGRADE=1 | |
| ) | |
| - name: Upload microsoft/git installers | |
| if: steps.check.outputs.result == '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: MicrosoftGit | |
| path: MicrosoftGit | |
| build: | |
| runs-on: windows-2025 | |
| name: Build and Unit Test | |
| needs: validate | |
| strategy: | |
| matrix: | |
| configuration: [ Debug, Release ] | |
| fail-fast: false | |
| steps: | |
| - name: Skip this job if there is a previous successful run | |
| if: needs.validate.outputs.skip != '' | |
| id: skip | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`) | |
| return true | |
| - name: Checkout source | |
| if: steps.skip.outputs.result != 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| path: src | |
| - name: Install .NET SDK | |
| if: steps.skip.outputs.result != 'true' | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| global-json-file: src/global.json | |
| - name: Add MSBuild to PATH | |
| if: steps.skip.outputs.result != 'true' | |
| uses: microsoft/setup-msbuild@v3.0.0 | |
| - name: Build VFS for Git | |
| if: steps.skip.outputs.result != 'true' | |
| shell: cmd | |
| run: src\scripts\Build.bat ${{ matrix.configuration }} | |
| - name: Run unit tests | |
| if: steps.skip.outputs.result != 'true' | |
| shell: cmd | |
| run: src\scripts\RunUnitTests.bat ${{ matrix.configuration }} | |
| - name: Create build artifacts | |
| if: steps.skip.outputs.result != 'true' | |
| shell: cmd | |
| run: src\scripts\CreateBuildArtifacts.bat ${{ matrix.configuration }} artifacts | |
| - name: Upload functional tests drop | |
| if: steps.skip.outputs.result != 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: FunctionalTests_${{ matrix.configuration }} | |
| path: artifacts\GVFS.FunctionalTests | |
| - name: Upload FastFetch drop | |
| if: steps.skip.outputs.result != 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: FastFetch_${{ matrix.configuration }} | |
| path: artifacts\FastFetch | |
| - name: Upload GVFS installer | |
| if: steps.skip.outputs.result != 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: GVFS_${{ matrix.configuration }} | |
| path: artifacts\GVFS.Installers | |
| functional_tests: | |
| name: Functional Tests | |
| needs: [validate, build] | |
| uses: ./.github/workflows/functional-tests.yaml | |
| with: | |
| skip: ${{ needs.validate.outputs.skip }} | |
| upgrade_tests: | |
| name: Upgrade Tests | |
| needs: [validate, build] | |
| uses: ./.github/workflows/upgrade-tests.yaml | |
| with: | |
| skip: ${{ needs.validate.outputs.skip }} | |
| result: | |
| runs-on: ubuntu-latest | |
| name: Build, Unit and Functional Tests Successful | |
| needs: [functional_tests, upgrade_tests] | |
| steps: | |
| - name: Success! # for easier identification of successful runs in the Checks Required for Pull Requests | |
| run: echo "Workflow run is successful!" |