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
98 changes: 84 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,84 @@ jobs:
- run: dotnet build --configuration Release --no-restore
- run: dotnet test --configuration Release --no-build

publish:
name: publish
# The headline artifact: one executable per platform that runs on a machine with no .NET
# installed. Nothing to unpack beyond the zip, nothing to install.
publish-standalone:
name: publish (${{ matrix.rid }})
needs: [version, verify]
if: needs.version.outputs.proceed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: true
matrix:
rid: [win-x64, linux-x64, osx-arm64]
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

- name: Publish ${{ matrix.rid }}
shell: bash
env:
VERSION: ${{ needs.version.outputs.version }}
RID: ${{ matrix.rid }}
run: |
set -euo pipefail
dotnet publish src/TargetApiSimulator/TargetApiSimulator.csproj \
--configuration Release \
--runtime "$RID" \
--self-contained true \
-p:PublishSingleFile=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugType=embedded \
-p:Version="$VERSION" \
-p:InformationalVersion="$VERSION+$GITHUB_SHA" \
--output "out/$RID"

LAUNCHER=TargetApiSimulator
[[ "$RID" == win-* ]] && LAUNCHER=TargetApiSimulator.exe

if [[ ! -f "out/$RID/$LAUNCHER" ]]; then
echo "No launcher was produced for $RID; the zip would not be runnable." >&2
ls -la "out/$RID" >&2
exit 1
fi

- name: Package
shell: bash
env:
VERSION: ${{ needs.version.outputs.version }}
RID: ${{ matrix.rid }}
run: |
set -euo pipefail
NAME="TargetApiSimulator-$VERSION-$RID"

# IIS hosting leftovers from the Web SDK. Nothing here is hosted in IIS, and they only
# make the download look like it needs assembling.
rm -f "out/$RID/web.config" \
"out/$RID/aspnetcorev2_inprocess.dll" \
"out/$RID/TargetApiSimulator.staticwebassets.endpoints.json"

[[ "$RID" == win-* ]] || chmod +x "out/$RID/TargetApiSimulator"

mkdir -p dist
( cd "out/$RID" && zip -r "$GITHUB_WORKSPACE/dist/$NAME.zip" . )
( cd dist && sha256sum "$NAME.zip" > "$NAME.zip.sha256" )

- uses: actions/upload-artifact@v7
with:
name: package-${{ matrix.rid }}
path: dist/*
retention-days: 14
if-no-files-found: error

# A small framework-dependent build for anyone who already has the .NET runtime and would
# rather download 30 KB than 50 MB.
publish-portable:
name: publish (portable)
needs: [version, verify]
if: needs.version.outputs.proceed == 'true'
runs-on: ubuntu-latest
Expand All @@ -128,9 +204,6 @@ jobs:
with:
dotnet-version: '10.0.x'

# One portable package, and deliberately no apphost. Without the native launcher a
# per-RID publish produces the same file set anyway, and a freshly built unsigned .exe
# with no download reputation gets flagged as Trojan:Script/Wacatac.B!ml.
- name: Publish
shell: bash
env:
Expand All @@ -145,32 +218,28 @@ jobs:
-p:InformationalVersion="$VERSION+$GITHUB_SHA" \
--output out/portable

if ls out/portable | grep -qiE '\.exe$'; then
echo "An .exe ended up in the package; UseAppHost=false is not taking effect." >&2
exit 1
fi

- name: Package
shell: bash
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
NAME="TargetApiSimulator-$VERSION"
NAME="TargetApiSimulator-$VERSION-portable"
rm -f out/portable/web.config out/portable/TargetApiSimulator.staticwebassets.endpoints.json
mkdir -p dist
( cd out/portable && zip -r "$GITHUB_WORKSPACE/dist/$NAME.zip" . )
( cd dist && sha256sum "$NAME.zip" > "$NAME.zip.sha256" )

- uses: actions/upload-artifact@v7
with:
name: package
name: package-portable
path: dist/*
retention-days: 14
if-no-files-found: error

release:
name: release
needs: [version, publish]
needs: [version, publish-standalone, publish-portable]
if: needs.version.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -179,7 +248,8 @@ jobs:
steps:
- uses: actions/download-artifact@v8
with:
name: package
pattern: package-*
merge-multiple: true
path: dist

# Creates the tag at this commit as part of publishing, so VERSION.md stays the only
Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.3] - 2026-07-31

### Changed

- **Releases ship a runnable executable again.** 1.0.1 and 1.0.2 contained a DLL and a handful of
JSON files, which is not something anyone can start. Each platform now gets a self-contained,
single-file build — `TargetApiSimulator.exe` on Windows, `TargetApiSimulator` on Linux and
macOS — that runs on a machine with no .NET installed at all. Roughly 50 MB, because the
runtime is inside it.
- The small framework-dependent build is still published as
`TargetApiSimulator-<version>-portable.zip` for anyone who already has the .NET 10 runtime.
- Console timestamps and the `Microsoft.AspNetCore` log filter are now defaults in code rather
than only in `appsettings.json`, so the standalone executable logs identically when it runs
with no files beside it. Configuration still overrides both.
- Packages no longer carry `web.config`, `aspnetcorev2_inprocess.dll` or the static web assets
manifest — IIS hosting leftovers that made the download look like it needed assembling.
- The release pipeline fails if a platform build produces no launcher, replacing the earlier
check that failed if one was present.

## [1.0.2] - 2026-07-31

### Fixed
Expand Down Expand Up @@ -119,7 +138,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Every pull request runs `dotnet list package --vulnerable --include-transitive` and fails on a
hit. CLI output is forced to English so the check cannot silently pass on a localised runner.

[Unreleased]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.2...HEAD
[Unreleased]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.3...HEAD
[1.0.3]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/Mysttic/TargetApiSimulator/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/Mysttic/TargetApiSimulator/releases/tag/v1.0.0
45 changes: 20 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,40 +221,35 @@ The container runs as an unprivileged user (`uid 1654`) and listens on 8080 only

## Get a release

Every release on the [Releases page](https://github.com/Mysttic/TargetApiSimulator/releases) ships one portable
zip plus its `.sha256` checksum. The .NET 10 runtime has to be installed; the same zip then runs on Windows,
Linux and macOS.
Download the zip for your platform from the
[Releases page](https://github.com/Mysttic/TargetApiSimulator/releases), unpack it, and run the executable.
**Nothing else has to be installed — not even .NET.**

Fetch and start the latest one in a single step — PowerShell:

```powershell
$tag = (Invoke-RestMethod https://api.github.com/repos/Mysttic/TargetApiSimulator/releases/latest).tag_name
Invoke-WebRequest "https://github.com/Mysttic/TargetApiSimulator/releases/download/$tag/TargetApiSimulator-$($tag.TrimStart('v')).zip" -OutFile tas.zip
Expand-Archive tas.zip -DestinationPath tas -Force
dotnet tas/TargetApiSimulator.dll --urls http://localhost:5000
```

bash:
| Asset | Run |
|---|---|
| `TargetApiSimulator-<version>-win-x64.zip` | `TargetApiSimulator.exe` |
| `TargetApiSimulator-<version>-linux-x64.zip` | `./TargetApiSimulator` |
| `TargetApiSimulator-<version>-osx-arm64.zip` | `./TargetApiSimulator` |
| `TargetApiSimulator-<version>-portable.zip` | `dotnet TargetApiSimulator.dll` — 30 KB, needs the .NET 10 runtime |

```bash
TAG=$(curl -sL https://api.github.com/repos/Mysttic/TargetApiSimulator/releases/latest | grep -oP '"tag_name": "\K[^"]+')
curl -sL "https://github.com/Mysttic/TargetApiSimulator/releases/download/$TAG/TargetApiSimulator-${TAG#v}.zip" -o tas.zip
unzip -o tas.zip -d tas
dotnet tas/TargetApiSimulator.dll --urls http://localhost:5000
./TargetApiSimulator --urls http://localhost:5000
```

Verify the download if you want to:
It listens on port 5000, prints everything it receives to the console, and stops with Ctrl+C. The three
platform builds are self-contained, which is why they are around 50 MB: the .NET runtime is inside the
executable. If you already have .NET 10, the `portable` zip does the same job in 30 KB.

Each asset has a `.sha256` file next to it:

```bash
sha256sum -c TargetApiSimulator-1.0.0.zip.sha256
sha256sum -c TargetApiSimulator-1.0.3-linux-x64.zip.sha256
```

> The zip deliberately contains **no `.exe` launcher**. A freshly built, unsigned executable with no download
> reputation is routinely flagged by antivirus machine-learning heuristics — Microsoft Defender reports it as
> `Trojan:Script/Wacatac.B!ml`. Shipping only the managed assembly removes the false positive entirely, at the
> cost of typing `dotnet` in front of the command.

Everything the application receives is printed to the console it runs in.
> **If antivirus blocks the download:** the executables are unsigned, and Microsoft Defender flags freshly
> built unsigned binaries with no download reputation as `Trojan:Script/Wacatac.B!ml` — a machine-learning
> guess, not a signature match. Check the `.sha256`, or use the container image, which is never affected:
> `docker run --rm -p 5000:8080 ghcr.io/mysttic/targetapisimulator`.

## Versioning

Expand Down
2 changes: 1 addition & 1 deletion VERSION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Version

1.0.2
1.0.3

<!--
This file is the single source of truth for the released version.
Expand Down
9 changes: 9 additions & 0 deletions src/TargetApiSimulator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

var builder = WebApplication.CreateBuilder(args);

// Set in code, not only in appsettings.json, so the standalone executable behaves the same when
// it runs on its own. Anything under Logging: in configuration still overrides this.
builder.Logging.AddSimpleConsole(options =>
{
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
options.UseUtcTimestamp = true;
});
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);

// The stub handles payloads in the kilobyte range. Kestrel's default is 30 MB, which is three
// thousand times more than needed and the only real memory multiplier in this process.
builder.WebHost.ConfigureKestrel(options =>
Expand Down