diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a093721..d7d3484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: name: Calculate package version runs-on: ubuntu-latest outputs: + patch: ${{ steps.gitversion.outputs.patch }} semver: ${{ steps.gitversion.outputs.semVer }} steps: @@ -297,3 +298,83 @@ jobs: exit 1 } Write-Output 'All required CI validations succeeded.' + + publish: + name: Publish patch-zero release + if: github.ref == 'refs/heads/main' && needs.version.outputs.patch == '0' + needs: [version, gate] + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - name: Check out publication guard + uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.sha }} + + # Every registry or release publication step must remain below this guard. + - name: Enforce main-only publication + shell: pwsh + run: ./scripts/Assert-MainPublication.ps1 + + - name: Download validated release packages + uses: actions/download-artifact@v8 + with: + name: packages-${{ needs.version.outputs.semver }} + path: artifacts + + - name: Create tagged GitHub release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ github.sha }} + VERSION: ${{ needs.version.outputs.semver }} + shell: pwsh + run: | + $tag = "v$env:VERSION" + $existingTag = gh api "repos/$env:GITHUB_REPOSITORY/git/ref/tags/$tag" ` + --jq '.object.sha' 2>$null + if ($LASTEXITCODE -eq 0) { + if ($existingTag -ne $env:RELEASE_SHA) { + throw "Tag '$tag' already points to '$existingTag', not '$env:RELEASE_SHA'." + } + gh release view $tag | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Tag '$tag' exists for this commit but its GitHub release is missing." + } + Write-Output "Release '$tag' already exists for this commit; leaving its artifacts unchanged." + exit 0 + } + + gh release create $tag artifacts/* ` + --target $env:RELEASE_SHA ` + --title $tag ` + --generate-notes + if ($LASTEXITCODE -ne 0) { throw "GitHub release '$tag' creation failed." } + + - name: Log in to NuGet with trusted publishing + id: nuget-login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 + with: + user: Seddryck + + - name: Publish NuGet package + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + shell: pwsh + run: | + if ([string]::IsNullOrWhiteSpace($env:NUGET_API_KEY)) { + throw 'NuGet trusted publishing did not provide a temporary API key.' + } + $packages = @(Get-ChildItem -LiteralPath artifacts -Filter *.nupkg) + if ($packages.Count -ne 1) { + throw "Expected exactly one NuGet package, found $($packages.Count)." + } + dotnet nuget push $packages[0].FullName ` + --api-key $env:NUGET_API_KEY ` + --source https://api.nuget.org/v3/index.json ` + --skip-duplicate + if ($LASTEXITCODE -ne 0) { throw 'NuGet publication failed.' } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 19db245..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,303 +0,0 @@ -name: Release - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: release - cancel-in-progress: false - -jobs: - native: - name: Build native library (${{ matrix.rid }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - rid: win-x64 - library: tree-sitter-expressif.dll - - os: windows-11-arm - rid: win-arm64 - library: tree-sitter-expressif.dll - - os: ubuntu-latest - rid: linux-x64 - library: libtree-sitter-expressif.so - - os: ubuntu-24.04-arm - rid: linux-arm64 - library: libtree-sitter-expressif.so - - os: macos-15-intel - rid: osx-x64 - library: libtree-sitter-expressif.dylib - - os: macos-15 - rid: osx-arm64 - library: libtree-sitter-expressif.dylib - - steps: - - name: Check out release commit - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Set up Tree-sitter - uses: tree-sitter/setup-action/cli@v2 - - - name: Build native grammar library - shell: pwsh - run: | - $outputDirectory = Join-Path 'native-assets' '${{ matrix.rid }}' - New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null - tree-sitter build -o (Join-Path $outputDirectory '${{ matrix.library }}') . - if ($LASTEXITCODE -ne 0) { throw 'Native grammar build failed.' } - - - name: Upload native grammar library - uses: actions/upload-artifact@v7 - with: - name: native-${{ matrix.rid }} - path: native-assets/${{ matrix.rid }}/${{ matrix.library }} - if-no-files-found: error - retention-days: 7 - - validate: - name: Build release packages - if: github.ref == 'refs/heads/main' - needs: native - runs-on: ubuntu-latest - outputs: - patch: ${{ steps.gitversion.outputs.patch }} - sha: ${{ steps.release-context.outputs.sha }} - version: ${{ steps.gitversion.outputs.semVer }} - - steps: - - name: Resolve release commit - id: release-context - shell: pwsh - run: | - if ([string]::IsNullOrWhiteSpace($env:GITHUB_SHA)) { - throw 'Could not resolve the release commit.' - } - "sha=$env:GITHUB_SHA" >> $env:GITHUB_OUTPUT - - - name: Check out release commit - uses: actions/checkout@v7 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ steps.release-context.outputs.sha }} - - - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v4 - with: - versionSpec: 6.x - - - name: Calculate release version - id: gitversion - uses: gittools/actions/gitversion/execute@v4 - with: - useConfigFile: true - - - name: Set up .NET - uses: actions/setup-dotnet@v6 - with: - dotnet-version: 10.0.x - - - name: Download Windows native grammar library - uses: actions/download-artifact@v8 - with: - name: native-win-x64 - path: native-assets/win-x64 - - - name: Download Linux native grammar library - uses: actions/download-artifact@v8 - with: - name: native-linux-x64 - path: native-assets/linux-x64 - - - name: Download Windows ARM64 native grammar library - uses: actions/download-artifact@v8 - with: - name: native-win-arm64 - path: native-assets/win-arm64 - - - name: Download Linux ARM64 native grammar library - uses: actions/download-artifact@v8 - with: - name: native-linux-arm64 - path: native-assets/linux-arm64 - - - name: Download macOS x64 native grammar library - uses: actions/download-artifact@v8 - with: - name: native-osx-x64 - path: native-assets/osx-x64 - - - name: Download macOS ARM64 native grammar library - uses: actions/download-artifact@v8 - with: - name: native-osx-arm64 - path: native-assets/osx-arm64 - - - name: Build NuGet package - shell: pwsh - run: >- - ./scripts/Build-NuGetPackage.ps1 - -Version '${{ steps.gitversion.outputs.semVer }}' - -NativeAssetsDirectory 'native-assets' - - - name: Build native C archive - shell: pwsh - run: ./scripts/New-NativeArchive.ps1 -Version '${{ steps.gitversion.outputs.semVer }}' - - - name: Upload validated release packages - uses: actions/upload-artifact@v7 - with: - name: release-packages-${{ steps.gitversion.outputs.semVer }} - path: artifacts/* - if-no-files-found: error - retention-days: 7 - - consume: - name: Test release package (${{ matrix.rid }}) - needs: validate - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - rid: win-x64 - - os: windows-11-arm - rid: win-arm64 - - os: ubuntu-latest - rid: linux-x64 - - os: ubuntu-24.04-arm - rid: linux-arm64 - - os: macos-15-intel - rid: osx-x64 - - os: macos-15 - rid: osx-arm64 - - steps: - - name: Check out release commit - uses: actions/checkout@v7 - with: - persist-credentials: false - ref: ${{ needs.validate.outputs.sha }} - - - name: Set up .NET runtimes - uses: actions/setup-dotnet@v6 - with: - dotnet-version: | - 8.0.x - 9.0.x - 10.0.x - - - name: Download release package - uses: actions/download-artifact@v8 - with: - name: release-packages-${{ needs.validate.outputs.version }} - path: artifacts - - - name: Restore installed package tests - shell: pwsh - run: | - $packageSource = (Resolve-Path 'artifacts').Path - dotnet restore bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj ` - -p:RestoreAdditionalProjectSources=$packageSource ` - -p:PackageVersionUnderTest='${{ needs.validate.outputs.version }}' - if ($LASTEXITCODE -ne 0) { throw 'Package test restore failed.' } - - - name: Test installed package - shell: pwsh - run: >- - dotnet test bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj - --configuration Release - --no-restore - -p:PackageVersionUnderTest='${{ needs.validate.outputs.version }}' - - publish: - name: Publish patch-zero release - if: needs.validate.outputs.patch == '0' - needs: [validate, consume] - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - - steps: - - name: Check out publication guard - uses: actions/checkout@v7 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ needs.validate.outputs.sha }} - - # Every registry or release publication step must remain below this guard. - - name: Enforce main-only publication - shell: pwsh - run: ./scripts/Assert-MainPublication.ps1 - - - name: Download validated release packages - uses: actions/download-artifact@v8 - with: - name: release-packages-${{ needs.validate.outputs.version }} - path: artifacts - - - name: Create tagged GitHub release - env: - GH_TOKEN: ${{ github.token }} - RELEASE_SHA: ${{ needs.validate.outputs.sha }} - VERSION: ${{ needs.validate.outputs.version }} - shell: pwsh - run: | - $tag = "v$env:VERSION" - $existingTag = gh api "repos/$env:GITHUB_REPOSITORY/git/ref/tags/$tag" ` - --jq '.object.sha' 2>$null - if ($LASTEXITCODE -eq 0) { - if ($existingTag -ne $env:RELEASE_SHA) { - throw "Tag '$tag' already points to '$existingTag', not '$env:RELEASE_SHA'." - } - gh release view $tag | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "Tag '$tag' exists for this commit but its GitHub release is missing." - } - Write-Output "Release '$tag' already exists for this commit; leaving its artifacts unchanged." - exit 0 - } - - gh release create $tag artifacts/* ` - --target $env:RELEASE_SHA ` - --title $tag ` - --generate-notes - if ($LASTEXITCODE -ne 0) { throw "GitHub release '$tag' creation failed." } - - - name: Log in to NuGet with trusted publishing - id: nuget-login - uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1 - with: - user: Seddryck - - - name: Publish NuGet package - env: - NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} - shell: pwsh - run: | - if ([string]::IsNullOrWhiteSpace($env:NUGET_API_KEY)) { - throw 'NuGet trusted publishing did not provide a temporary API key.' - } - $packages = @(Get-ChildItem -LiteralPath artifacts -Filter *.nupkg) - if ($packages.Count -ne 1) { - throw "Expected exactly one NuGet package, found $($packages.Count)." - } - dotnet nuget push $packages[0].FullName ` - --api-key $env:NUGET_API_KEY ` - --source https://api.nuget.org/v3/index.json ` - --skip-duplicate - if ($LASTEXITCODE -ne 0) { throw 'NuGet publication failed.' } diff --git a/README.md b/README.md index 4875939..cd0b455 100644 --- a/README.md +++ b/README.md @@ -136,12 +136,10 @@ The grammar should remain independent from the Expressif function catalogue. Par ## Releases -For every push to `main`, the release workflow independently validates the parser and all bindings, builds the release packages, and calculates the repository version with GitVersion. When validation succeeds and the calculated semantic version has a patch component of `0`, it creates the corresponding `vX.Y.0` tag and GitHub release. Other versions complete package validation without creating a tag or release. +For every push to `main`, CI builds, tests, and collects the distributable artifacts. After every validation job succeeds, a patch-zero version is published from those same collected artifacts to the corresponding `vX.Y.0` GitHub release and NuGet.org. Other versions complete validation without publishing artifacts. -Each GitHub release contains every validated package produced by `scripts/package.ps1`: +Each GitHub release contains the distributable artifacts collected by CI after package validation: -* the TypeScript/Node package -* the Python wheel and source distribution * the C# NuGet package * the native parser source archive @@ -149,10 +147,10 @@ Only the C# package is currently published to an external registry. NuGet public * Repository Owner: `Seddryck` * Repository: `Expressif.Syntax` -* Workflow File: `release.yml` +* Workflow File: `ci.yml` * Environment: leave blank -The policy's NuGet user must be `Seddryck`, matching the `NuGet/login` step in the workflow. Python and TypeScript/Node packages remain available as GitHub release artifacts until PyPI and npm publishing are implemented. +The policy's NuGet user must be `Seddryck`, matching the `NuGet/login` step in the workflow. ## Related projects