Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 8 additions & 22 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
# Set default behavior to automatically normalize line endings.
* text=auto
# Every text file is stored and checked out with LF, whatever platform it is checked out on, so a file
# reads the same everywhere and a generator writing one produces the same bytes on any machine. Declaring
# the default rather than a list of extensions means a file type nobody has added yet is already right.
* text=auto eol=lf

# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.cs text eol=lf
*.csx text eol=lf
*.razor text eol=lf
*.xaml text eol=lf
*.xml text eol=lf
*.config text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.txt text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.csproj text eol=lf
*.sln text eol=lf

# Declare files that will always have CRLF line endings on checkout.
# The exception, and the only one: cmd.exe parses a batch file line by line as it runs it, and mishandles
# a label or a goto that ends in LF alone.
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# Denote all files that are truly binary and should not be modified.
*.png binary
Expand Down Expand Up @@ -51,3 +35,5 @@
*.pptx binary
*.snk binary
*.pfx binary
*.mp4 binary
*.wav binary
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Runs the gate a person runs, then lints Forma, which lints clean, so anything reported there is a regression.
# Runs the fast gate a person runs, then lints Forma, which lints clean, so anything reported there is a regression. It
# runs on every push but main's, which promotion fast-forwards to a commit candidate's full gate passed, and on every
# pull request.
name: CI

on:
push:
branches: [main]
branches-ignore: [main]
pull_request:
branches: [main]
workflow_dispatch:

# A newer push to a pull request supersedes the older run; every commit on a branch keeps its own result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Runs the full gate on the candidate: the fast gate on Linux and Windows, the package packed and verified as a
# consumer receives it, and Forma linted. Its passing run on a push to candidate is the one the promotion and release
# checks look for on the exact commit, and the packages it keeps are the ones the release publishes.
name: Full

on:
push:
branches: [candidate]
workflow_dispatch:

# A candidate superseded on candidate before its run finished is no longer the one to promote.
concurrency:
group: full-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
gate:
name: gate (${{ matrix.os }})
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
# The solution references Forma's runtime beside it, so both repositories are checked out side by side, Forma at the branch its runtime is developed on.
- uses: actions/checkout@v7
with:
path: Forma.Locali

- uses: actions/checkout@v7
with:
repository: FormaUI/Forma
ref: dev
path: formaui-net

# The SDK Forma pins, which is the one the linter is built and run with.
- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: formaui-net/global.json

# The package is packed and verified once, on Linux, and the release publishes that package.
- name: Run the full gate
if: runner.os == 'Linux'
working-directory: Forma.Locali
run: pwsh ./build.ps1 -Full

- name: Run the gate
if: runner.os != 'Linux'
working-directory: Forma.Locali
run: pwsh ./build.ps1

- name: Lint Forma
working-directory: Forma.Locali
run: |
dotnet restore ../formaui-net/Forma.slnx
dotnet run --project src/Forma.Locali --no-build -- --path ../formaui-net/Forma.slnx --exclude "*/Fixtures/*"

- name: Keep the verified packages
if: runner.os == 'Linux'
uses: actions/upload-artifact@v7
with:
name: packages
path: Forma.Locali/artifacts/packages/
if-no-files-found: error
retention-days: 90
30 changes: 30 additions & 0 deletions .github/workflows/promotion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Holds every pull request into main to the promotion check of FormaUI/Versioning, at the release Locali follows: it
# comes from candidate, candidate's full gate passed on its head commit and still holds the packages it built, and the
# version it declares is new, with a dated changelog entry.
name: Promotion

on:
pull_request:
branches: [main]
types: [opened, edited, reopened, synchronize]

permissions:
contents: read
actions: read
pull-requests: read

jobs:
promotion:
name: Promotion
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: FormaUI/Versioning/actions/promotion-check@68ff74cadbe94084384d29be93466713fd989b53 # v1.0.0-preview.1
with:
gate-workflow: full.yml
version-command: "([xml] (Get-Content src/Forma.Locali/Forma.Locali.csproj)).SelectSingleNode('//PropertyGroup/Version').InnerText"
packages-artifact: packages
108 changes: 64 additions & 44 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,65 +1,51 @@
# Publishes a release tag's package to nuget.org through Trusted Publishing, once the gate passes on the tagged commit.
# Publishes a release from its signed vX.Y.Z tag on main. The release check of FormaUI/Versioning, at the release Locali
# follows, holds the tag first; the package candidate's full gate built and verified is then attached to the tag's
# GitHub release with its digest, and published to nuget.org through trusted publishing. A failed publication runs again
# for the same tag.
name: Release

on:
push:
tags: ['*.*.*']
workflow_dispatch:
inputs:
tag:
description: The release tag to publish, such as 1.0.0-preview.10.
required: true
tags: ['v*']

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
publish:
release:
name: Release
runs-on: ubuntu-latest

permissions:
contents: read
# nuget.org exchanges the job's OIDC token for a key that lasts an hour.
# The release-packages step writes the tag's GitHub release.
contents: write
actions: read
pull-requests: read
# nuget.org exchanges the job's OIDC token for a key that lasts an hour, through its policy for this workflow.
id-token: write

env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}

steps:
# The solution references Forma's runtime beside it, so both repositories are checked out side by side, Forma at the branch its runtime is developed on.
- uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
path: Forma.Locali

- uses: actions/checkout@v7
- name: Hold the tag to the release check
id: check
uses: FormaUI/Versioning/actions/release-check@68ff74cadbe94084384d29be93466713fd989b53 # v1.0.0-preview.1
with:
repository: FormaUI/Forma
ref: dev
path: formaui-net
gate-workflow: full.yml
version-command: "([xml] (Get-Content src/Forma.Locali/Forma.Locali.csproj)).SelectSingleNode('//PropertyGroup/Version').InnerText"
packages-artifact: packages

- name: Set up .NET
uses: actions/setup-dotnet@v6
- name: Attach the release's packages
id: packages
uses: FormaUI/Versioning/actions/release-packages@68ff74cadbe94084384d29be93466713fd989b53 # v1.0.0-preview.1
with:
global-json-file: formaui-net/global.json

- name: Check the tag names the project's version
working-directory: Forma.Locali
shell: pwsh
run: |
[xml] $project = Get-Content src/Forma.Locali/Forma.Locali.csproj
$version = $project.SelectSingleNode('//PropertyGroup/Version').InnerText -replace '\+.*$', ''
if ($version -ne $env:RELEASE_TAG) {
throw "The tag $env:RELEASE_TAG does not name the project's version, $version."
}

- name: Run the gate
working-directory: Forma.Locali
run: pwsh ./build.ps1

- name: Pack
working-directory: Forma.Locali
run: dotnet pack src/Forma.Locali -c Release -o "${{ runner.temp }}/package" -nologo
gate-run-id: ${{ steps.check.outputs.gate-run-id }}
packages-artifact: packages
name: Locali

# The user is the nuget.org account that made the policy.
- name: Get a nuget.org key
Expand All @@ -68,5 +54,39 @@ jobs:
with:
user: soyeyalon

- name: Push to nuget.org
run: dotnet nuget push "${{ runner.temp }}/package/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json
# A package nuget.org holds already was published by an earlier run for this tag; pushing a package pushes its
# symbol package beside it.
- name: Publish to nuget.org
shell: pwsh
env:
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
PACKAGES: ${{ steps.packages.outputs.directory }}
run: |
Add-Type -AssemblyName System.IO.Compression.FileSystem
foreach ($package in Get-ChildItem -LiteralPath $env:PACKAGES -Filter '*.nupkg') {
$archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName)
try {
$entry = $archive.Entries | Where-Object { $_.FullName -notmatch '/' -and $_.Name -like '*.nuspec' } | Select-Object -First 1
$reader = [System.IO.StreamReader]::new($entry.Open())
try {
$metadata = ([xml] $reader.ReadToEnd()).package.metadata
}
finally {
$reader.Dispose()
}
}
finally {
$archive.Dispose()
}

$index = Invoke-RestMethod "https://api.nuget.org/v3-flatcontainer/$($metadata.id.ToLowerInvariant())/index.json" -SkipHttpErrorCheck -StatusCodeVariable status
if ($status -eq 200 -and $index.versions -contains $metadata.version.ToLowerInvariant()) {
Write-Host "nuget.org already holds $($metadata.id) $($metadata.version)."
continue
}

dotnet nuget push $package.FullName --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json
if ($LASTEXITCODE -ne 0) {
throw "Pushing $($package.Name) to nuget.org failed."
}
}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

Each release of Locali, newest first. An entry is dated when its release is cut, and reads *Unreleased* until then.
The releases before `1.0.0-preview.11` are recorded by their [tags](https://github.com/FormaUI/Locali/tags) alone.

## `1.0.0-preview.11`

*November 5, 2026*

- The package links each of its assemblies to the commit it was built from through Source Link, carries its symbols
in a `.snupkg` beside it, and names this entry as its release notes.
- The package is published to nuget.org alone; the FormaUI organization's GitHub Packages feed receives no new
version.
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

<!-- A build on GitHub Actions maps its source paths to the repository's, as Source Link resolves them. -->
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

</Project>
Loading
Loading