Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d578debe2 | ||
|
|
5adc4a6526 | ||
|
|
66e3af043c | ||
|
|
9a632b8d62 | ||
|
|
cb0e838964 | ||
|
|
8954f9aaba | ||
|
|
94843311b4 | ||
|
|
52e5f68da0 | ||
|
|
99b3030180 |
+18
@@ -414,3 +414,21 @@ FodyWeavers.xsd
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
|
||||
# ---> AutoBidder / Mimante
|
||||
|
||||
# Configurazione di rilascio: contiene un token di Gitea
|
||||
gitea.json
|
||||
|
||||
# Riepilogo della rigiocata sui dossier (target Backtest)
|
||||
*backtest-report.txt
|
||||
|
||||
# Pacchetti prodotti da build/Release.proj
|
||||
bin/installer/
|
||||
|
||||
# Rider / JetBrains
|
||||
.idea/
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
Vendored
+39
-11
@@ -17,7 +17,10 @@
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
@@ -32,7 +35,10 @@
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"group": { "kind": "test", "isDefault": true },
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
@@ -47,7 +53,10 @@
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -63,12 +72,15 @@
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "crea installatore",
|
||||
"detail": "Chiede la versione, verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Non tocca Gitea.",
|
||||
"detail": "Verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Crea il tag a pacchetto pronto. Non tocca Gitea.",
|
||||
"type": "process",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
@@ -79,7 +91,10 @@
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
@@ -96,12 +111,15 @@
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "rilascia su Gitea",
|
||||
"detail": "Chiede versione e note, poi: verifica, pubblica, installatore, tag e release su Gitea con i file allegati. Richiede build/gitea.json.",
|
||||
"detail": "Verifica, pubblica, installatore, tag e release su Gitea con i file allegati. La versione viene dal tag su HEAD. Richiede build/gitea.json.",
|
||||
"type": "process",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
@@ -109,11 +127,21 @@
|
||||
"${workspaceFolder}/build/Release.proj",
|
||||
"-t:Rilascia",
|
||||
"-p:Versione=${input:versione}",
|
||||
"-p:Note=${input:note}",
|
||||
"-nologo",
|
||||
"-v:m"
|
||||
],
|
||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
||||
// Le note passano dall'ambiente, non da -p:. MSBuild spezza il valore di
|
||||
// una proprietà sulle virgole e una nota in italiano ne ha quasi sempre
|
||||
// una: si otterrebbe MSB1006 «proprietà non valida». Vedi Release.proj.
|
||||
"options": {
|
||||
"env": {
|
||||
"AUTOBIDDER_NOTE": "${input:note}"
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
],
|
||||
@@ -121,7 +149,7 @@
|
||||
{
|
||||
"id": "versione",
|
||||
"type": "promptString",
|
||||
"description": "Versione (vuoto = incrementa la minor, es. 4.11.0 → 4.12.0)",
|
||||
"description": "Versione — lascia vuoto se hai già taggato (git tag v4.14.0), o per la minor successiva",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
|
||||
<!-- App desktop leggera per Bidoo. Nessun software aggiuntivo:
|
||||
WebView2 runtime è preinstallato su Windows 11. -->
|
||||
<!-- Unico numero da toccare a ogni nuova versione: titolo finestra e log d'avvio
|
||||
lo leggono a runtime tramite Utilities/AppInfo. -->
|
||||
<Version>4.12.0</Version>
|
||||
<AssemblyVersion>4.12.0.0</AssemblyVersion>
|
||||
<FileVersion>4.12.0.0</FileVersion>
|
||||
<InformationalVersion>4.12.0</InformationalVersion>
|
||||
<!-- Versione delle sole compilazioni di sviluppo. NON va alzata per rilasciare:
|
||||
nei pacchetti pubblicati questi quattro numeri vengono dal tag git, passati
|
||||
da build/Release.proj a dotnet publish. Vedi Utilities/AppInfo. -->
|
||||
<Version>4.13.0</Version>
|
||||
<AssemblyVersion>4.13.0.0</AssemblyVersion>
|
||||
<FileVersion>4.13.0.0</FileVersion>
|
||||
<InformationalVersion>4.13.0</InformationalVersion>
|
||||
|
||||
<!-- Runtime unico supportato per la pubblicazione self-contained -->
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
</Button>
|
||||
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
ToolTip="Ferma tutte: nessuna richiesta"
|
||||
Click="StopButton_Click">
|
||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||
@@ -390,6 +391,7 @@
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||
</Button>
|
||||
<Button Style="{StaticResource MiniIconButton}"
|
||||
Foreground="{DynamicResource Brush.Danger}"
|
||||
ToolTip="Ferma"
|
||||
IsEnabled="{Binding CanStop}"
|
||||
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo."/>
|
||||
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo. Il valore predefinito è 0,20 €, cioè il pacchetto meno vantaggioso in vendita su Bidoo: tenerlo così fa stimare per eccesso, e una stima per eccesso al massimo ti fa rinunciare a un affare, mentre una per difetto ti fa spendere più del valore del prodotto. Se compri pacchetti più grandi, abbassalo al costo che paghi davvero."/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||
Text="0,20" Margin="10,10"/>
|
||||
</Grid>
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||
Content="Aste seguite per meno del"
|
||||
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali. Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti. Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
|
||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
|
||||
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Brush.Text}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||
Content="Aste chiuse da più di"
|
||||
|
||||
@@ -45,6 +45,17 @@ namespace AutoBidder.Dialogs
|
||||
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
||||
};
|
||||
|
||||
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
|
||||
// il filtro resta spento finché il numero non ha senso.
|
||||
if (ChkCoverage.IsChecked == true &&
|
||||
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
|
||||
percento is > 0 and <= 100)
|
||||
{
|
||||
options.MinObservedCoverage = percento / 100.0;
|
||||
}
|
||||
|
||||
if (ChkOlderThan.IsChecked == true &&
|
||||
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Cose da fare:
|
||||
- Non caricare i sorgenti del programma nella release. Deve solo essere presente la versione portable e la versione installabile.
|
||||
- Il nome del setup deve essere AutoBidder_Versione.exe (ad esempio AutoBidder_4.14.0.exe)
|
||||
- Colora il pulsante stop di rosso (così come quello verde è verde) per quanto riguarda i globali. Colora anche quelli relativi alla singola asta presenti sulla griglia di monitoraggio AutoBidder_Versione
|
||||
- Verifica tutta l'applicazione e tutto il codice per cercare di ottimizzare l'applicazione per renderla più veloce e meno bisognosa di risorse
|
||||
- Aggiungi la possibilità di pulire le aste non valide o con non abbastanza dati, come per esempio quelle non seguite completamente dall'inizio alla fine. In generale devo poterle pulire
|
||||
- Agiungi una impostazione per indicare il costo di una puntata (fisso a 20 centesimi, che corrisponde al piano meno vantaggioso per l'acquisto di puntate dal sito di Bidoo)
|
||||
|
||||
@@ -56,6 +56,26 @@ namespace AutoBidder.Services
|
||||
var failed = 0;
|
||||
var skipped = 0;
|
||||
|
||||
// ── Perché si salva a blocchi ────────────────────────────────
|
||||
// Un Append per asta rilegge e riscrive l'intero storico ogni volta:
|
||||
// su un recupero completo (4675 aste) erano minuti di solo rimescolamento
|
||||
// JSON e svariati gigabyte scritti sul disco. Un blocco ogni cinquanta
|
||||
// taglia i salvataggi di cinquanta volte senza mettere a rischio più di
|
||||
// cinquanta richieste già pagate: quello che non è ancora stato salvato
|
||||
// viene comunque scritto all'uscita, annullamento compreso.
|
||||
const int DimensioneBlocco = 50;
|
||||
var daSalvare = new List<CompletedAuctionRecord>(DimensioneBlocco);
|
||||
|
||||
void Deposita()
|
||||
{
|
||||
if (daSalvare.Count == 0) return;
|
||||
|
||||
CompletedAuctionsStore.AppendMany(daSalvare);
|
||||
daSalvare.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < missing.Count; i++)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
@@ -78,15 +98,19 @@ namespace AutoBidder.Services
|
||||
var check = AuctionIntegrity.CheckWinnerBids(record);
|
||||
if (check.IsTrustworthy)
|
||||
{
|
||||
// Si riscrive solo il record toccato: Append sostituisce
|
||||
// quello con lo stesso id.
|
||||
CompletedAuctionsStore.Append(record);
|
||||
// Va nel blocco: AppendMany sostituisce i record con lo
|
||||
// stesso id, quindi il risultato è identico a un Append
|
||||
// per ciascuno, con un salvataggio invece di cinquanta.
|
||||
daSalvare.Add(record);
|
||||
if (daSalvare.Count >= DimensioneBlocco) Deposita();
|
||||
|
||||
updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Il valore c'è ma non regge il confronto col prezzo: non si
|
||||
// salva, e si dice perché. Meglio un buco che una media storta.
|
||||
// Il valore c'è ma non regge il confronto col prezzo: non
|
||||
// si salva, e si dice perché. Meglio un buco che una media
|
||||
// storta.
|
||||
record.WinnerBidsPaid = null;
|
||||
record.WinnerBidsFree = null;
|
||||
skipped++;
|
||||
@@ -108,6 +132,14 @@ namespace AutoBidder.Services
|
||||
|
||||
if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Anche se si esce per annullamento o per un errore: le richieste già
|
||||
// fatte sono state pagate al server, buttarne i risultati significa
|
||||
// rifarle al giro dopo.
|
||||
Deposita();
|
||||
}
|
||||
|
||||
return new Result(missing.Count, updated, failed, skipped);
|
||||
}
|
||||
|
||||
@@ -309,4 +309,73 @@ public class StatsMaintenanceTests
|
||||
Assert.Equal(report.Kept, kept.Count);
|
||||
Assert.Equal(report.Removed, list.Count - kept.Count);
|
||||
}
|
||||
|
||||
// ── Aste seguite solo in parte ───────────────────────────────────────
|
||||
//
|
||||
// Il prezzo finale in centesimi e' il totale delle puntate dell'asta: confrontarlo
|
||||
// con i reset osservati dice quanta asta si e' vista, senza bisogno di registrare
|
||||
// nulla di nuovo. Vale quindi anche sullo storico raccolto prima della regola.
|
||||
|
||||
private static CompletedAuctionRecord Seguita(string id, double price, int resets)
|
||||
{
|
||||
var r = R(id: id, price: price, bidsFree: 1);
|
||||
r.TotalResets = resets;
|
||||
return r;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1.00, 100, 1.0)] // 1,00 EUR = 100 puntate, viste tutte
|
||||
[InlineData(1.00, 50, 0.5)] // agganciata a meta'
|
||||
[InlineData(1.00, 0, 0.0)] // vista solo la fine
|
||||
public void La_copertura_e_i_reset_sul_totale_di_puntate(
|
||||
double price, int resets, double attesa)
|
||||
{
|
||||
Assert.Equal(attesa, AuctionIntegrity.ObservedCoverage(price, resets)!.Value, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Senza_prezzo_la_copertura_non_si_puo_dire()
|
||||
{
|
||||
Assert.Null(AuctionIntegrity.ObservedCoverage(0, 42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void La_copertura_non_supera_mai_uno()
|
||||
{
|
||||
// L'arrotondamento del prezzo puo' produrre qualche reset di troppo: non e'
|
||||
// un'asta osservata piu' del possibile, e non deve diventare 103%.
|
||||
Assert.Equal(1.0, AuctionIntegrity.ObservedCoverage(1.00, 103)!.Value, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Toglie_le_aste_seguite_sotto_la_soglia()
|
||||
{
|
||||
var list = new List<CompletedAuctionRecord>
|
||||
{
|
||||
Seguita("intera", 1.00, 100),
|
||||
Seguita("quasi", 1.00, 95),
|
||||
Seguita("meta", 1.00, 50),
|
||||
};
|
||||
|
||||
var options = new StatsMaintenance.CleanupOptions
|
||||
{
|
||||
MinObservedCoverage = 0.90
|
||||
};
|
||||
|
||||
var kept = StatsMaintenance.Apply(list, options);
|
||||
|
||||
Assert.Equal(2, kept.Count);
|
||||
Assert.DoesNotContain(kept, r => r.AuctionId == "meta");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spenta_la_soglia_non_toglie_nulla_per_copertura()
|
||||
{
|
||||
var list = new List<CompletedAuctionRecord> { Seguita("meta", 1.00, 1) };
|
||||
|
||||
var report = StatsMaintenance.Preview(list,
|
||||
new StatsMaintenance.CleanupOptions { MinObservedCoverage = null });
|
||||
|
||||
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using AutoBidder.Models;
|
||||
using AutoBidder.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace AutoBidder.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Archivio delle aste concluse: copia in memoria e salvataggio a blocchi.
|
||||
///
|
||||
/// <para>Sono le due modifiche che hanno tolto dal percorso caldo la rilettura dell'intero
|
||||
/// file — ~72 ms su 5876 record, pagati sul thread dell'interfaccia a ogni asta che finisce.
|
||||
/// La proprieta' da difendere e' che andare piu' veloce non cambi <b>cosa</b> viene salvato:
|
||||
/// AppendMany deve dare lo stesso risultato di tanti Append, e la copia in memoria non deve
|
||||
/// mai servire un contenuto che sul disco non c'e' piu'.</para>
|
||||
/// </summary>
|
||||
public class CompletedAuctionsStoreTests
|
||||
{
|
||||
private static CompletedAuctionRecord R(string id, double price = 1.00, string? nome = null) =>
|
||||
new()
|
||||
{
|
||||
AuctionId = id,
|
||||
Name = nome ?? $"Asta {id}",
|
||||
ProductKey = "prova",
|
||||
FinalPrice = price,
|
||||
Winner = "tizio",
|
||||
EndedAt = DateTime.Now
|
||||
};
|
||||
|
||||
private static void Svuota() => CompletedAuctionsStore.ReplaceAll(new List<CompletedAuctionRecord>());
|
||||
|
||||
[Fact]
|
||||
public void Append_sostituisce_quella_con_lo_stesso_id()
|
||||
{
|
||||
Svuota();
|
||||
|
||||
CompletedAuctionsStore.Append(R("100", price: 1.00));
|
||||
CompletedAuctionsStore.Append(R("100", price: 2.50));
|
||||
|
||||
var tutte = CompletedAuctionsStore.LoadAll();
|
||||
|
||||
Assert.Single(tutte);
|
||||
Assert.Equal(2.50, tutte[0].FinalPrice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendMany_da_lo_stesso_risultato_di_tanti_Append()
|
||||
{
|
||||
var lotto = new[] { R("a", 1), R("b", 2), R("c", 3) };
|
||||
|
||||
Svuota();
|
||||
foreach (var r in lotto) CompletedAuctionsStore.Append(r);
|
||||
var unoAllaVolta = CompletedAuctionsStore.LoadAll()
|
||||
.OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
|
||||
|
||||
Svuota();
|
||||
CompletedAuctionsStore.AppendMany(lotto);
|
||||
var inBlocco = CompletedAuctionsStore.LoadAll()
|
||||
.OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
|
||||
|
||||
Assert.Equal(unoAllaVolta, inBlocco);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendMany_sostituisce_e_aggiunge_nello_stesso_giro()
|
||||
{
|
||||
Svuota();
|
||||
CompletedAuctionsStore.AppendMany(new[] { R("x", 1), R("y", 2) });
|
||||
|
||||
CompletedAuctionsStore.AppendMany(new[] { R("y", 9), R("z", 3) });
|
||||
|
||||
var tutte = CompletedAuctionsStore.LoadAll().ToDictionary(r => r.AuctionId, r => r.FinalPrice);
|
||||
|
||||
Assert.Equal(3, tutte.Count);
|
||||
Assert.Equal(9, tutte["y"]);
|
||||
Assert.Equal(1, tutte["x"]);
|
||||
Assert.Equal(3, tutte["z"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendMany_senza_niente_da_fare_non_esplode()
|
||||
{
|
||||
Svuota();
|
||||
CompletedAuctionsStore.AppendMany(Array.Empty<CompletedAuctionRecord>());
|
||||
|
||||
Assert.Empty(CompletedAuctionsStore.LoadAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void I_record_senza_id_restano_tutti()
|
||||
{
|
||||
// Un id vuoto non identifica niente: due record cosi' non sono lo stesso record,
|
||||
// e accorparli farebbe sparire un'asta davvero conclusa.
|
||||
Svuota();
|
||||
CompletedAuctionsStore.AppendMany(new[] { R(""), R("") });
|
||||
|
||||
Assert.Equal(2, CompletedAuctionsStore.LoadAll().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void La_copia_in_memoria_si_accorge_se_il_file_cambia_da_fuori()
|
||||
{
|
||||
Svuota();
|
||||
CompletedAuctionsStore.Append(R("uno"));
|
||||
Assert.Single(CompletedAuctionsStore.LoadAll());
|
||||
|
||||
// Ripristino di un backup o modifica a mano: la copia non deve continuare a
|
||||
// servire il contenuto vecchio. La data si forza per non dipendere dalla
|
||||
// granularita' con cui il file system aggiorna l'ora di scrittura.
|
||||
File.WriteAllText(CompletedAuctionsStore.StorePath,
|
||||
JsonSerializer.Serialize(new List<CompletedAuctionRecord> { R("due"), R("tre") }));
|
||||
File.SetLastWriteTimeUtc(CompletedAuctionsStore.StorePath, DateTime.UtcNow.AddSeconds(5));
|
||||
|
||||
var dopo = CompletedAuctionsStore.LoadAll();
|
||||
|
||||
Assert.Equal(2, dopo.Count);
|
||||
Assert.DoesNotContain(dopo, r => r.AuctionId == "uno");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dopo_ReplaceAll_si_legge_quello_nuovo()
|
||||
{
|
||||
Svuota();
|
||||
CompletedAuctionsStore.AppendMany(new[] { R("vecchia") });
|
||||
|
||||
CompletedAuctionsStore.ReplaceAll(new List<CompletedAuctionRecord> { R("nuova") });
|
||||
|
||||
var tutte = CompletedAuctionsStore.LoadAll();
|
||||
|
||||
Assert.Single(tutte);
|
||||
Assert.Equal("nuova", tutte[0].AuctionId);
|
||||
}
|
||||
}
|
||||
@@ -457,6 +457,12 @@
|
||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||
<Setter Property="SelectionMode" Value="Single"/>
|
||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
||||
<!-- Il riciclo riusa i contenitori di riga invece di ricrearli a ogni giro di
|
||||
scorrimento. Con la griglia del monitor, che si aggiorna piu' volte al
|
||||
secondo mentre le aste corrono, e' la differenza fra allocare righe nuove
|
||||
di continuo e non allocarne quasi nessuna. Il valore predefinito di WPF e'
|
||||
"Standard", cioe' senza riciclo. -->
|
||||
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
|
||||
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
||||
</Style>
|
||||
|
||||
@@ -5,10 +5,14 @@ namespace AutoBidder.Utilities
|
||||
/// <summary>
|
||||
/// Informazioni sull'applicazione, in un solo posto.
|
||||
///
|
||||
/// <para>La versione si legge dall'assembly (dalla proprietà <c>Version</c> del
|
||||
/// <c>.csproj</c>): così il titolo della finestra e il log d'avvio restano allineati
|
||||
/// da soli, e per pubblicare una nuova versione basta cambiare un unico numero nel
|
||||
/// progetto invece di rincorrerlo in più stringhe sparse.</para>
|
||||
/// <para>La versione si legge dall'assembly, così il titolo della finestra e il log
|
||||
/// d'avvio restano allineati da soli invece di rincorrere il numero in più stringhe
|
||||
/// sparse.</para>
|
||||
///
|
||||
/// <para>Nei pacchetti pubblicati quel numero viene dal tag git, scritto
|
||||
/// nell'assembly da <c>build/Release.proj</c> al momento della pubblicazione. Il
|
||||
/// <c><Version></c> del <c>.csproj</c> vale solo per le compilazioni di
|
||||
/// sviluppo: alzarlo non pubblica niente, e non serve prima di un rilascio.</para>
|
||||
/// </summary>
|
||||
public static class AppInfo
|
||||
{
|
||||
|
||||
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
|
||||
/// </summary>
|
||||
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
||||
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
||||
|
||||
/// <summary>
|
||||
/// Quanta parte dell'asta è stata davvero osservata, da 0 a 1. <c>null</c> se non
|
||||
/// si può dire (prezzo finale mancante).
|
||||
///
|
||||
/// <para>Sfrutta la stessa identità del controllo qui sopra: il prezzo finale in
|
||||
/// centesimi è il numero totale di puntate dell'asta. Confrontarlo con i reset
|
||||
/// visti dice, senza bisogno di registrare nient'altro, se l'asta è stata seguita
|
||||
/// dall'inizio o presa per strada. Un'asta agganciata a metà ha numeri veri ma
|
||||
/// parziali — è il caso peggiore, perché sembra buona.</para>
|
||||
///
|
||||
/// <para>Il valore è tagliato a 1: sui dati reali (5876 aste) nessun record supera
|
||||
/// 1,02, e il poco che sfora è arrotondamento del prezzo, non un'asta osservata
|
||||
/// più del possibile.</para>
|
||||
/// </summary>
|
||||
public static double? ObservedCoverage(double finalPrice, int totalResets)
|
||||
{
|
||||
if (finalPrice <= 0) return null;
|
||||
|
||||
var totalBids = Math.Round(finalPrice * 100);
|
||||
if (totalBids <= 0) return null;
|
||||
|
||||
return Math.Min(1.0, Math.Max(0, totalResets) / totalBids);
|
||||
}
|
||||
|
||||
/// <summary>Comodità: la copertura di un'asta già registrata.</summary>
|
||||
public static double? ObservedCoverage(CompletedAuctionRecord record) =>
|
||||
record == null ? null : ObservedCoverage(record.FinalPrice, record.TotalResets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,25 @@ namespace AutoBidder.Utilities
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
// ── Copia in memoria ─────────────────────────────────────────────
|
||||
//
|
||||
// Il file cresce di un record per asta e arriva in fretta a qualche megabyte:
|
||||
// a 5876 aste, leggerlo e deserializzarlo costa ~72 ms misurati. Senza copia
|
||||
// in memoria quel prezzo si paga a ogni apertura delle statistiche e a ogni
|
||||
// asta che finisce — e quella chiamata arriva da un gestore di eventi, cioe'
|
||||
// dal thread dell'interfaccia, che resta fermo per tutto il tempo.
|
||||
//
|
||||
// L'invalidazione guarda la data di modifica del file, non un tempo di
|
||||
// scadenza: cosi' la copia resta giusta anche se il file viene sostituito da
|
||||
// fuori — ripristino di un backup, modifica a mano — che qui capita davvero.
|
||||
//
|
||||
// Il prezzo da sapere: i record restituiti sono gli STESSI oggetti che stanno
|
||||
// nella copia. Chi ne modifica uno sta modificando anche la copia, prima e
|
||||
// indipendentemente dal salvataggio. Repair e il recupero puntate contano su
|
||||
// questo; chi non lo vuole si faccia una copia del record.
|
||||
private static List<CompletedAuctionRecord>? _cache;
|
||||
private static DateTime _cacheStamp;
|
||||
|
||||
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
||||
public static string StorePath => FilePath;
|
||||
|
||||
@@ -34,16 +53,50 @@ namespace AutoBidder.Utilities
|
||||
public static void Append(CompletedAuctionRecord record)
|
||||
{
|
||||
if (record == null) return;
|
||||
AppendMany(new[] { record });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiunge o sostituisce piu' aste con <b>un solo</b> salvataggio.
|
||||
///
|
||||
/// <para>Serve a chi ne tocca tante di fila, come il recupero delle puntate dei
|
||||
/// vincitori. Chiamare <see cref="Append"/> in un ciclo rilegge e riscrive tutto
|
||||
/// il file a ogni giro: sulle 4675 aste di un recupero completo erano minuti di
|
||||
/// solo rimescolamento JSON e svariati gigabyte scritti sul disco per nulla.</para>
|
||||
/// </summary>
|
||||
public static void AppendMany(IReadOnlyCollection<CompletedAuctionRecord> records)
|
||||
{
|
||||
if (records == null || records.Count == 0) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var list = LoadInternal();
|
||||
|
||||
var existing = list.FindIndex(r =>
|
||||
!string.IsNullOrEmpty(r.AuctionId) && r.AuctionId == record.AuctionId);
|
||||
// Indice per id: cercare ogni record con un FindIndex renderebbe
|
||||
// quadratico proprio il caso per cui questo metodo esiste.
|
||||
var byId = new Dictionary<string, int>(list.Count, StringComparer.Ordinal);
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
var id = list[i].AuctionId;
|
||||
if (!string.IsNullOrEmpty(id)) byId[id] = i;
|
||||
}
|
||||
|
||||
if (existing >= 0) list[existing] = record;
|
||||
else list.Add(record);
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (record == null) continue;
|
||||
|
||||
var id = record.AuctionId;
|
||||
|
||||
if (!string.IsNullOrEmpty(id) && byId.TryGetValue(id, out var at))
|
||||
{
|
||||
list[at] = record;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id)) byId[id] = list.Count;
|
||||
list.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
SaveInternal(list);
|
||||
}
|
||||
@@ -119,10 +172,12 @@ namespace AutoBidder.Utilities
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath)) File.Delete(FilePath);
|
||||
_cache = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
||||
_cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,18 +276,33 @@ namespace AutoBidder.Utilities
|
||||
try
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
_cache = null;
|
||||
return new List<CompletedAuctionRecord>();
|
||||
}
|
||||
|
||||
// Una lettura di data di modifica costa microsecondi; la lettura piu'
|
||||
// la deserializzazione che evita ne costa decine di millisecondi.
|
||||
var stamp = File.GetLastWriteTimeUtc(FilePath);
|
||||
if (_cache != null && stamp == _cacheStamp) return _cache;
|
||||
|
||||
var json = File.ReadAllText(FilePath);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return new List<CompletedAuctionRecord>();
|
||||
|
||||
return JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||
var list = string.IsNullOrWhiteSpace(json)
|
||||
? new List<CompletedAuctionRecord>()
|
||||
: JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||
?? new List<CompletedAuctionRecord>();
|
||||
|
||||
_cache = list;
|
||||
_cacheStamp = stamp;
|
||||
return list;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
||||
|
||||
// Non si sa cosa ci sia sul disco: la prossima volta si rilegge.
|
||||
_cache = null;
|
||||
return new List<CompletedAuctionRecord>();
|
||||
}
|
||||
}
|
||||
@@ -246,10 +316,16 @@ namespace AutoBidder.Utilities
|
||||
|
||||
var json = JsonSerializer.Serialize(list, JsonOptions);
|
||||
File.WriteAllText(FilePath, json);
|
||||
|
||||
// La copia in memoria e' esattamente quello che si e' appena scritto:
|
||||
// tenerla evita di rileggere il file al primo uso successivo.
|
||||
_cache = list;
|
||||
_cacheStamp = File.GetLastWriteTimeUtc(FilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
||||
_cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,17 @@ namespace AutoBidder.Utilities
|
||||
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
||||
public bool RemoveDuplicates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Copertura minima richiesta, da 0 a 1: sotto questa soglia l'asta è stata
|
||||
/// agganciata a metà e va tolta. Null = non filtrare sulla copertura.
|
||||
///
|
||||
/// <para>È il criterio per «non seguita dall'inizio alla fine». Non serve
|
||||
/// nessun campo nuovo nei record: la copertura si ricava dal prezzo finale,
|
||||
/// vedi <see cref="AuctionIntegrity.ObservedCoverage(double,int)"/>. Vale
|
||||
/// quindi anche per lo storico raccolto prima che questa regola esistesse.</para>
|
||||
/// </summary>
|
||||
public double? MinObservedCoverage { get; set; }
|
||||
|
||||
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||
public DateTime? OlderThan { get; set; }
|
||||
}
|
||||
@@ -161,6 +172,15 @@ namespace AutoBidder.Utilities
|
||||
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
||||
Mark(r, "senza valore del prodotto");
|
||||
|
||||
if (options.MinObservedCoverage is { } soglia)
|
||||
{
|
||||
// Copertura non calcolabile = asta senza prezzo finale: qui vale come
|
||||
// "non seguita", perché non c'è modo di dire il contrario.
|
||||
var copertura = AuctionIntegrity.ObservedCoverage(r);
|
||||
if (copertura is null || copertura.Value < soglia)
|
||||
Mark(r, "seguita solo in parte");
|
||||
}
|
||||
|
||||
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
||||
Mark(r, "più vecchia del limite");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
; ─────────────────────────────────────────────────────────────────────────────
|
||||
; AutoBidder — script di installazione (Inno Setup 6)
|
||||
;
|
||||
; Non si compila a mano: lo lancia build/Release.proj, che prima pubblica
|
||||
; l'eseguibile e poi passa qui versione e percorsi con /D. Compilarlo da solo
|
||||
; produrrebbe un pacchetto con la versione sbagliata, perché il numero vive
|
||||
; nel .csproj.
|
||||
;
|
||||
; dotnet msbuild build/Release.proj -t:Pacchetto
|
||||
;
|
||||
; L'applicazione è un unico .exe self-contained: l'installazione copia quel file,
|
||||
; crea i collegamenti e registra la voce per "App installate". I dati dell'utente
|
||||
; stanno in %LocalAppData%\AutoBidder e NON vengono toccati né dall'aggiornamento
|
||||
; né dalla disinstallazione — sono aste, statistiche e sessione, cioè mesi di
|
||||
; raccolta che nessun disinstallatore ha il diritto di buttare via.
|
||||
; ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef SourceExe
|
||||
#define SourceExe "..\bin\publish\win-x64\AutoBidder.exe"
|
||||
#endif
|
||||
|
||||
#ifndef OutputDir
|
||||
#define OutputDir "..\bin\installer"
|
||||
#endif
|
||||
|
||||
#define AppName "AutoBidder"
|
||||
#define AppPublisher "Alberto Balbo"
|
||||
#define AppExeName "AutoBidder.exe"
|
||||
|
||||
[Setup]
|
||||
; L'AppId identifica il prodotto fra una versione e l'altra: cambiarlo farebbe
|
||||
; comparire due voci in "App installate" invece di un aggiornamento.
|
||||
AppId={{9F2C6E14-8B3D-4A57-9E10-6C4B0D7A1F52}
|
||||
AppName={#AppName}
|
||||
AppVersion={#AppVersion}
|
||||
AppVerName={#AppName} {#AppVersion}
|
||||
AppPublisher={#AppPublisher}
|
||||
VersionInfoVersion={#AppVersion}
|
||||
|
||||
; Installazione per utente: niente richiesta di amministratore, e i dati stanno
|
||||
; comunque nel profilo di chi la usa.
|
||||
PrivilegesRequired=lowest
|
||||
DefaultDirName={autopf}\{#AppName}
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DisableDirPage=auto
|
||||
|
||||
OutputDir={#OutputDir}
|
||||
OutputBaseFilename=AutoBidder_{#AppVersion}
|
||||
SetupIconFile=..\Icon\favicon.ico
|
||||
UninstallDisplayIcon={app}\{#AppExeName}
|
||||
UninstallDisplayName={#AppName} {#AppVersion}
|
||||
|
||||
; L'eseguibile è già compresso (single-file compresso): comprimerlo di nuovo
|
||||
; costa minuti di build e guadagna poco.
|
||||
Compression=lzma2/normal
|
||||
SolidCompression=no
|
||||
WizardStyle=modern
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
[Languages]
|
||||
Name: "italiano"; MessagesFile: "compiler:Languages\Italian.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "Crea un collegamento sul desktop"; GroupDescription: "Collegamenti:"
|
||||
Name: "avvioautomatico"; Description: "Avvia AutoBidder all'accesso a Windows"; GroupDescription: "Avvio:"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#SourceExe}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||
Name: "{group}\Disinstalla {#AppName}"; Filename: "{uninstallexe}"
|
||||
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Registry]
|
||||
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
|
||||
ValueType: string; ValueName: "{#AppName}"; ValueData: """{app}\{#AppExeName}"""; \
|
||||
Flags: uninsdeletevalue; Tasks: avvioautomatico
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#AppExeName}"; Description: "Avvia {#AppName}"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
; Solo quello che ha creato l'installazione. I dati restano.
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Code]
|
||||
{ Un'installazione sopra una copia in esecuzione lascerebbe il vecchio eseguibile
|
||||
al suo posto senza dirlo, e l'utente si ritroverebbe la versione precedente
|
||||
convinto di aver aggiornato. Meglio fermarsi e chiedere. }
|
||||
function IsAppRunning(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := Exec('cmd.exe',
|
||||
'/C tasklist /FI "IMAGENAME eq {#AppExeName}" | find /I "{#AppExeName}"',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
|
||||
if IsAppRunning() then
|
||||
begin
|
||||
if MsgBox('AutoBidder è in esecuzione e sta forse seguendo delle aste.' + #13#10#13#10 +
|
||||
'Chiudilo prima di continuare, altrimenti il file non può essere sostituito.' + #13#10#13#10 +
|
||||
'Riprovo?', mbConfirmation, MB_YESNO) = IDYES then
|
||||
Result := not IsAppRunning()
|
||||
else
|
||||
Result := False;
|
||||
|
||||
if not Result then
|
||||
MsgBox('Installazione annullata: AutoBidder è ancora aperto.', mbInformation, MB_OK);
|
||||
end;
|
||||
end;
|
||||
@@ -0,0 +1,87 @@
|
||||
# Catena di verifica, pacchetto e rilascio
|
||||
|
||||
Tutto quello che serve a controllare, impacchettare e pubblicare AutoBidder sta in questa
|
||||
cartella. La radice del progetto non contiene script.
|
||||
|
||||
| File | Cos'è |
|
||||
|---|---|
|
||||
| `Release.proj` | La catena. Un solo file MSBuild, nessuno script. |
|
||||
| `AutoBidder.iss` | Lo script di Inno Setup. Non si compila a mano: lo lancia `Release.proj`. |
|
||||
| `gitea.example.json` | Modello per `gitea.json` (che è escluso dal controllo di versione). |
|
||||
| `sostituiti/` | I vecchi script PowerShell, tenuti finché non si fa la pulizia. Non servono più. |
|
||||
|
||||
## Da VS Code
|
||||
|
||||
**Terminale ▸ Esegui attività…**
|
||||
|
||||
| Attività | Cosa fa |
|
||||
|---|---|
|
||||
| `verifica` | Compila e lancia i test. |
|
||||
| `backtest` | Rigioca i dossier delle aste concluse. |
|
||||
| `backtest (giro veloce, 400 aste)` | Come sopra, su un campione. |
|
||||
| `crea installatore` | Chiede la versione, verifica, pubblica, esegue Inno Setup. |
|
||||
| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. |
|
||||
| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. |
|
||||
|
||||
## Da riga di comando
|
||||
|
||||
```powershell
|
||||
dotnet msbuild build/Release.proj -t:Verifica
|
||||
dotnet msbuild build/Release.proj -t:Backtest -p:MaxDossier=400
|
||||
dotnet msbuild build/Release.proj -t:Pacchetto
|
||||
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=4.13.0 -p:Note="Cosa cambia"
|
||||
```
|
||||
|
||||
| Proprietà | Predefinito | A cosa serve |
|
||||
|---|---|---|
|
||||
| `Versione` | vuoto | Vuoto = incrementa la minor (4.12.0 → 4.13.0). Altrimenti la scrive, se ha la forma `X.Y.Z`. |
|
||||
| `Note` | vuoto | Note di rilascio. |
|
||||
| `SaltaVerifica` | `false` | Non rieseguire i test. |
|
||||
| `Sovrascrivi` | `false` | Sostituisci una release Gitea con lo stesso tag. |
|
||||
| `Bozza` | `false` | Crea la release come bozza. |
|
||||
| `MaxDossier` | `0` | Quanti dossier leggere nel backtest (0 = tutti). |
|
||||
| `CartellaDossier` | `%LocalAppData%\AutoBidder\Dati\Registri\Aste` | Dove stanno i dossier. |
|
||||
|
||||
## Chi chiede la versione
|
||||
|
||||
MSBuild non può chiedere niente a nessuno: è un motore di compilazione. La domanda la fa
|
||||
l'attività di VS Code (`inputs` in `.vscode/tasks.json`) e passa la risposta in
|
||||
`-p:Versione=`. Lasciando il campo vuoto si prende la minor successiva, che è il caso
|
||||
normale di fine sessione.
|
||||
|
||||
**La versione si scrive solo dopo che i test sono passati.** Il contrario sembra più
|
||||
naturale — decidi il numero, poi costruisci — ma lascia il `.csproj` alzato quando la
|
||||
verifica fallisce, e il tentativo dopo riparte da lì: il numero sale senza che sia mai
|
||||
esistito un pacchetto con quella versione.
|
||||
|
||||
## Gitea
|
||||
|
||||
Servono quattro valori. Le variabili d'ambiente hanno la precedenza sul file, così una
|
||||
macchina condivisa può rilasciare senza scrivere un token su disco:
|
||||
|
||||
- `GITEA_URL`, `GITEA_OWNER`, `GITEA_REPO`, `GITEA_TOKEN`
|
||||
- oppure `build/gitea.json`, copiato da `gitea.example.json`
|
||||
|
||||
Il token si crea in Gitea da *Impostazioni ▸ Applicazioni ▸ Genera nuovo token*, con il
|
||||
permesso `repository: read and write`.
|
||||
|
||||
Nella release vengono caricati **sia l'installatore sia l'eseguibile nudo**: chi non vuole
|
||||
installare niente deve continuare a poter scaricare il solo `.exe`.
|
||||
|
||||
## Una trappola già pagata
|
||||
|
||||
`dotnet test` e `dotnet publish` lanciati **da dentro** MSBuild ereditano l'ambiente del
|
||||
processo padre. La compilazione WPF crea un progetto temporaneo (`_wpftmp.csproj`) e con
|
||||
`MSBUILD_EXE_PATH` puntata al build in corso non genera più le classi parziali dello XAML:
|
||||
si ottengono decine di *«AuctionMonitorControl non contiene una definizione di …»* che non
|
||||
hanno niente a che vedere col codice.
|
||||
|
||||
Per questo gli `Exec` azzerano `MSBUILD_EXE_PATH` e `MSBuildLoadMicrosoftTargetsReadOnly`.
|
||||
**Solo quelle due**: la ricetta che gira in rete azzera anche `MSBuildExtensionsPath` e
|
||||
`MSBuildSDKsPath`, e così il figlio perde la posizione dell'SDK — *«l'SDK Microsoft.NET.Sdk
|
||||
specificato non è stato trovato»*. Serve isolare il motore, non nascondergli dove abita.
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
- .NET SDK 10
|
||||
- [Inno Setup 6](https://jrsoftware.org/isinfo.php) — `winget install -e --id JRSoftware.InnoSetup`
|
||||
@@ -0,0 +1,634 @@
|
||||
<!--
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
AutoBidder — catena di verifica, pacchetto e rilascio
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Un solo file, nessuno script. Si richiama con `dotnet msbuild`, dalle attività
|
||||
di VS Code (Terminale ▸ Esegui attività…) oppure a mano:
|
||||
|
||||
dotnet msbuild build/Release.proj -t:Verifica
|
||||
dotnet msbuild build/Release.proj -t:Backtest
|
||||
dotnet msbuild build/Release.proj -t:Pacchetto
|
||||
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=4.12.0
|
||||
|
||||
── Perché MSBuild e non uno script ──────────────────────────────────────
|
||||
La catena vive accanto al codice che rilascia ed è versionata con lui: fra
|
||||
sei mesi, ripescato un tag, questo file ricostruisce quel pacchetto e non
|
||||
quello di oggi. La logica non banale (leggere e riscrivere la versione,
|
||||
parlare con Gitea) sta in attività C# in linea: si legge come codice, non
|
||||
come una successione di comandi.
|
||||
|
||||
── Da dove viene la versione ────────────────────────────────────────────
|
||||
Dal tag git, e da nient'altro. Il .csproj non viene mai riscritto: il numero
|
||||
arriva a `dotnet publish` come proprietà da riga di comando, quindi tag,
|
||||
eseguibile, installatore e release portano lo stesso numero per costruzione,
|
||||
non per disciplina.
|
||||
|
||||
Il modo previsto è taggare e poi rilasciare:
|
||||
|
||||
git tag v4.14.0
|
||||
dotnet msbuild build/Release.proj -t:Rilascia
|
||||
|
||||
Se HEAD non ha un tag di versione la catena lo crea da sé — con il numero
|
||||
passato in `-p:Versione=`, oppure la minor successiva all'ultimo tag — e lo
|
||||
fa in fondo, quando l'installatore esiste davvero. Un giro andato male non
|
||||
lascia dietro un tag per una versione che non è mai stata costruita.
|
||||
|
||||
Il numero nel .csproj resta quello delle compilazioni di sviluppo, e non
|
||||
c'entra più con i rilasci.
|
||||
-->
|
||||
<Project DefaultTargets="Pacchetto" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<PropertyGroup>
|
||||
<Radice>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))</Radice>
|
||||
<Csproj>$(Radice)\AutoBidder.csproj</Csproj>
|
||||
<TestProj>$(Radice)\Tests\AutoBidder.Tests.csproj</TestProj>
|
||||
<Iss>$(MSBuildThisFileDirectory)AutoBidder.iss</Iss>
|
||||
|
||||
<CartellaPubblicazione>$(Radice)\bin\publish\win-x64</CartellaPubblicazione>
|
||||
<CartellaPacchetti>$(Radice)\bin\installer</CartellaPacchetti>
|
||||
|
||||
<!-- ── Perché una cartella a parte per verifica e backtest ─────────────
|
||||
Due ragioni, e servono entrambe.
|
||||
|
||||
La prima: l'applicazione può essere aperta mentre si lavora, e tiene
|
||||
bloccato AutoBidder.exe — la compilazione si fermerebbe su MSB3027.
|
||||
|
||||
La seconda: l'opzione artifacts-path sposta anche gli INTERMEDI, non
|
||||
solo il risultato. La sola -o li lascia nella obj/ condivisa, e la
|
||||
compilazione WPF — che genera un progetto temporaneo `_wpftmp.csproj` a
|
||||
ogni giro — ogni tanto ci trovava stato altrui e smetteva di produrre
|
||||
le classi parziali dello XAML. Il sintomo era una raffica di
|
||||
"AuctionMonitorControl non contiene una definizione di ...", a giri
|
||||
alterni, senza che il codice fosse cambiato. -->
|
||||
<CartellaProve>$([System.IO.Path]::GetTempPath())AutoBidder.Verifica</CartellaProve>
|
||||
|
||||
<!-- Serve solo quando HEAD non è ancora taggato: è il numero del tag da
|
||||
creare. Vuoto = la minor successiva all'ultimo tag. Vedi VersioneDaTag. -->
|
||||
<Versione Condition="'$(Versione)' == ''"></Versione>
|
||||
|
||||
<!--
|
||||
Note di rilascio. Vuote = solo il numero di versione.
|
||||
|
||||
Arrivano da una variabile d'ambiente, non da -p:. MSBuild spezza il valore
|
||||
di una proprietà sulle virgole: `-p:Note=uno, due` diventa la proprietà
|
||||
Note=uno più l'opzione " due", e si finisce su MSB1006 "proprietà non
|
||||
valida". Una nota di rilascio in italiano contiene quasi sempre una
|
||||
virgola, quindi il passaggio per riga di comando è inutilizzabile.
|
||||
L'ambiente non ha questo problema, e MSBuild legge le variabili
|
||||
d'ambiente come proprietà.
|
||||
|
||||
-p:Note= resta accettato per chi lo passa a mano senza virgole.
|
||||
-->
|
||||
<Note Condition="'$(Note)' == ''">$(AUTOBIDDER_NOTE)</Note>
|
||||
|
||||
<!-- ── Perché serve azzerare queste variabili ──────────────────────────
|
||||
`dotnet test` e `dotnet publish` lanciati da dentro MSBuild ereditano
|
||||
l'ambiente del processo padre. La compilazione WPF crea un progetto
|
||||
temporaneo (_wpftmp.csproj) e con quelle variabili puntate al build in
|
||||
corso non genera più le classi parziali dello XAML: si ottengono decine
|
||||
di "AuctionMonitorControl non contiene una definizione di ..." che non
|
||||
hanno niente a che vedere col codice.
|
||||
|
||||
Si azzerano SOLO queste due. Togliere anche MSBuildExtensionsPath o
|
||||
MSBuildSDKsPath — la ricetta che gira in rete — fa perdere al figlio la
|
||||
posizione dell'SDK: "l'SDK Microsoft.NET.Sdk specificato non è stato
|
||||
trovato". Serve isolare il motore, non nascondergli dove abita. -->
|
||||
<AmbientePulito>MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly=</AmbientePulito>
|
||||
|
||||
<SaltaVerifica Condition="'$(SaltaVerifica)' == ''">false</SaltaVerifica>
|
||||
<Sovrascrivi Condition="'$(Sovrascrivi)' == ''">false</Sovrascrivi>
|
||||
<Bozza Condition="'$(Bozza)' == ''">false</Bozza>
|
||||
|
||||
<!-- Un rilascio da albero sporco produce un tag che non corrisponde a
|
||||
nessuno stato ricostruibile: è esattamente la garanzia che il tag come
|
||||
fonte unica dovrebbe dare. Si può forzare, ma va detto. -->
|
||||
<ConsentiModifiche Condition="'$(ConsentiModifiche)' == ''">false</ConsentiModifiche>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- ═════════════════════ Attività in linea ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
Decide quale versione si sta costruendo, senza toccare niente.
|
||||
|
||||
Il tag su HEAD, se c'è, comanda: è la fonte. Se non c'è se ne propone uno —
|
||||
il numero chiesto, o la minor successiva al tag più alto esistente — che
|
||||
verrà creato solo a pacchetto pronto, da CreaTag.
|
||||
-->
|
||||
<UsingTask TaskName="VersioneDaTag" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<TagsHead ParameterType="System.String" />
|
||||
<TagsTutti ParameterType="System.String" />
|
||||
<Richiesta ParameterType="System.String" />
|
||||
<Csproj ParameterType="System.String" Required="true" />
|
||||
<Versione ParameterType="System.String" Output="true" />
|
||||
<Tag ParameterType="System.String" Output="true" />
|
||||
<DaCreare ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Using Namespace="System.Collections.Generic" />
|
||||
<Using Namespace="System.Text.RegularExpressions" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
// ConsoleToMSBuild unisce le righe con il punto e virgola; git le separa
|
||||
// con a-capo. Si accettano entrambi e non si fanno domande.
|
||||
Func<string, string[]> spezza = s =>
|
||||
(s ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var forma = new Regex(@"^v(\d+)\.(\d+)\.(\d+)$");
|
||||
|
||||
var suHead = new List<string>();
|
||||
foreach (var t in spezza(TagsHead))
|
||||
if (forma.IsMatch(t.Trim())) suHead.Add(t.Trim());
|
||||
|
||||
if (suHead.Count > 1)
|
||||
{
|
||||
Log.LogError(
|
||||
"HEAD ha piu' di un tag di versione (" + string.Join(", ", suHead) + ").\n" +
|
||||
"Non si puo' sapere quale sia il rilascio: tienine uno solo con git tag -d <tag>");
|
||||
return false;
|
||||
}
|
||||
|
||||
var chiesta = (Richiesta ?? "").Trim();
|
||||
if (chiesta.Length > 0 && !Regex.IsMatch(chiesta, @"^\d+\.\d+\.\d+$"))
|
||||
{
|
||||
// Un refuso qui produrrebbe un tag e un pacchetto sbagliati.
|
||||
Log.LogError("Versione '" + chiesta + "' non valida: serve la forma X.Y.Z");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (suHead.Count == 1)
|
||||
{
|
||||
var dalTag = suHead[0].Substring(1);
|
||||
|
||||
if (chiesta.Length > 0 && chiesta != dalTag)
|
||||
{
|
||||
Log.LogError(
|
||||
"HEAD e' gia' taggato " + suHead[0] + ", ma e' stata chiesta la versione " + chiesta + ".\n" +
|
||||
"Il tag e' la fonte: o rilasci " + dalTag + " lasciando vuota la versione,\n" +
|
||||
"oppure togli il tag con git tag -d " + suHead[0] + " e rilancia.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Versione = dalTag;
|
||||
Tag = suHead[0];
|
||||
DaCreare = "false";
|
||||
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — dal tag " + Tag + " su HEAD");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (chiesta.Length > 0)
|
||||
{
|
||||
Versione = chiesta;
|
||||
}
|
||||
else
|
||||
{
|
||||
// La minor successiva al tag piu' alto: cosi' il numero proposto e'
|
||||
// sempre libero, anche se il tag piu' alto sta su un altro ramo.
|
||||
int maggiore = -1, minore = -1;
|
||||
foreach (var t in spezza(TagsTutti))
|
||||
{
|
||||
var m = forma.Match(t.Trim());
|
||||
if (!m.Success) continue;
|
||||
|
||||
int ma = int.Parse(m.Groups[1].Value), mi = int.Parse(m.Groups[2].Value);
|
||||
if (ma > maggiore || (ma == maggiore && mi > minore)) { maggiore = ma; minore = mi; }
|
||||
}
|
||||
|
||||
if (maggiore < 0)
|
||||
{
|
||||
// Primo rilascio: non c'e' nessun tag da cui ripartire, e l'unico
|
||||
// numero che esiste e' quello delle compilazioni di sviluppo.
|
||||
var testo = File.Exists(Csproj) ? File.ReadAllText(Csproj) : "";
|
||||
var m = Regex.Match(testo, @"<Version>(\d+)\.(\d+)\.\d+</Version>");
|
||||
if (!m.Success)
|
||||
{
|
||||
Log.LogError(
|
||||
"Nessun tag di versione nel repository e <Version> illeggibile in " + Csproj + ".\n" +
|
||||
"Indica la versione con -p:Versione=X.Y.Z");
|
||||
return false;
|
||||
}
|
||||
|
||||
maggiore = int.Parse(m.Groups[1].Value);
|
||||
minore = int.Parse(m.Groups[2].Value);
|
||||
}
|
||||
|
||||
Versione = maggiore + "." + (minore + 1) + ".0";
|
||||
}
|
||||
|
||||
Tag = "v" + Versione;
|
||||
DaCreare = "true";
|
||||
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — il tag " + Tag + " sara' creato a pacchetto pronto");
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="TrovaInnoSetup" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Percorso ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var candidati = new[]
|
||||
{
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Inno Setup 6\ISCC.exe"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Inno Setup 6\ISCC.exe"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Inno Setup 6\ISCC.exe"),
|
||||
};
|
||||
|
||||
foreach (var c in candidati)
|
||||
if (File.Exists(c)) { Percorso = c; break; }
|
||||
|
||||
if (string.IsNullOrEmpty(Percorso))
|
||||
Log.LogError("Inno Setup 6 non trovato. Installalo con: winget install -e --id JRSoftware.InnoSetup");
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<!--
|
||||
Gitea si raggiunge con curl, di serie in Windows 10 e 11.
|
||||
|
||||
L'alternativa naturale sarebbe HttpClient in un'attività C# in linea, ma
|
||||
RoslynCodeTaskFactory referenzia solo gli assembly di base: System.Net.Http e
|
||||
System.Text.Json andrebbero indicati per percorso assoluto, dentro il runtime
|
||||
condiviso, con il numero di versione nel mezzo. Un percorso che oggi funziona e
|
||||
al prossimo aggiornamento di .NET no. curl non ha questo problema.
|
||||
|
||||
Il token NON passa mai dalla riga di comando: sta in un file di configurazione
|
||||
di curl, che viene cancellato subito dopo. Gli Exec hanno EchoOff perché un
|
||||
registro di compilazione è la classica cosa che si incolla in una chat.
|
||||
-->
|
||||
|
||||
<UsingTask TaskName="LeggiConfigGitea" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Percorso ParameterType="System.String" Required="true" />
|
||||
<Url ParameterType="System.String" Output="true" />
|
||||
<Owner ParameterType="System.String" Output="true" />
|
||||
<Repo ParameterType="System.String" Output="true" />
|
||||
<Token ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Using Namespace="System.Text.RegularExpressions" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var testo = File.Exists(Percorso) ? File.ReadAllText(Percorso) : "";
|
||||
|
||||
// Le variabili d'ambiente hanno la precedenza sul file: una macchina
|
||||
// condivisa deve poter rilasciare senza scrivere un token su disco, e un
|
||||
// file dimenticato non deve vincere su una scelta esplicita.
|
||||
Func<string,string,string> leggi = (env, campo) =>
|
||||
{
|
||||
var v = Environment.GetEnvironmentVariable(env);
|
||||
if (!string.IsNullOrWhiteSpace(v)) return v.Trim();
|
||||
|
||||
var m = Regex.Match(testo, "\"" + campo + "\"\\s*:\\s*\"([^\"]*)\"");
|
||||
return m.Success ? m.Groups[1].Value.Trim() : "";
|
||||
};
|
||||
|
||||
Url = leggi("GITEA_URL", "url").TrimEnd('/');
|
||||
Owner = leggi("GITEA_OWNER", "owner");
|
||||
Repo = leggi("GITEA_REPO", "repo");
|
||||
Token = leggi("GITEA_TOKEN", "token");
|
||||
|
||||
if (Url.Length == 0 || Owner.Length == 0 || Repo.Length == 0 || Token.Length == 0)
|
||||
{
|
||||
Log.LogError(
|
||||
"Configurazione di Gitea incompleta. Servono url, owner, repo, token:\n" +
|
||||
" copia build/gitea.example.json in build/gitea.json e riempilo,\n" +
|
||||
" oppure imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.\n" +
|
||||
"Non e' stato costruito niente: si controlla prima di compilare, non dopo.\n" +
|
||||
"Per il solo installatore, senza Gitea, usa il target Pacchetto.");
|
||||
|
||||
// Senza questo il target prosegue lo stesso: git tag, poi curl con
|
||||
// l'indirizzo vuoto, e infine un "codice 3" che non dice niente a
|
||||
// nessuno. Un errore va fermato dove si capisce ancora cos'era.
|
||||
return false;
|
||||
}
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="PreparaCorpoRelease" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Destinazione ParameterType="System.String" Required="true" />
|
||||
<Tag ParameterType="System.String" Required="true" />
|
||||
<Versione ParameterType="System.String" Required="true" />
|
||||
<Note ParameterType="System.String" />
|
||||
<Bozza ParameterType="System.Boolean" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
// Le note arrivano da un prompt: possono contenere virgolette, barre e
|
||||
// a-capo. Scritte grezze romperebbero il JSON, o peggio lo cambierebbero.
|
||||
Func<string,string> esc = t => (t ?? "")
|
||||
.Replace("\\", "\\\\").Replace("\"", "\\\"")
|
||||
.Replace("\r", "").Replace("\n", "\\n").Replace("\t", " ");
|
||||
|
||||
var note = string.IsNullOrWhiteSpace(Note) ? "Versione " + Versione + "." : Note;
|
||||
|
||||
File.WriteAllText(Destinazione,
|
||||
"{\"tag_name\":\"" + esc(Tag) + "\"," +
|
||||
"\"name\":\"AutoBidder " + esc(Versione) + "\"," +
|
||||
"\"body\":\"" + esc(note) + "\"," +
|
||||
"\"draft\":" + (Bozza ? "true" : "false") + "," +
|
||||
"\"prerelease\":false}");
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="LeggiIdRelease" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Risposta ParameterType="System.String" Required="true" />
|
||||
<Id ParameterType="System.String" Output="true" />
|
||||
<Errore ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Using Namespace="System.Text.RegularExpressions" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var testo = File.Exists(Risposta) ? File.ReadAllText(Risposta) : "";
|
||||
|
||||
// L'id della release e' il primo campo "id" della risposta; quelli annidati
|
||||
// (autore, allegati) vengono dopo. Si prende il primo e basta.
|
||||
var m = Regex.Match(testo, "\"id\"\\s*:\\s*(\\d+)");
|
||||
Id = m.Success ? m.Groups[1].Value : "";
|
||||
|
||||
if (Id.Length == 0)
|
||||
{
|
||||
var msg = Regex.Match(testo, "\"message\"\\s*:\\s*\"([^\"]*)\"");
|
||||
Errore = msg.Success ? msg.Groups[1].Value
|
||||
: (testo.Length > 200 ? testo.Substring(0, 200) : testo);
|
||||
}
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<!-- ═════════════════════ Verifica ═════════════════════ -->
|
||||
|
||||
<Target Name="Verifica" Condition="'$(SaltaVerifica)' != 'true'">
|
||||
<Message Importance="High" Text="== Verifica (compilazione + test) ==" />
|
||||
|
||||
<!-- In una cartella a parte: l'applicazione puo' essere aperta e tenere
|
||||
bloccato AutoBidder.exe. -->
|
||||
<Exec Command="dotnet test "$(TestProj)" --nologo -v q --artifacts-path "$(CartellaProve)""
|
||||
WorkingDirectory="$(Radice)"
|
||||
EnvironmentVariables="$(AmbientePulito)" />
|
||||
|
||||
<Message Importance="High" Text=" tutto a posto" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Rigiocata sui dossier ═════════════════════ -->
|
||||
|
||||
<Target Name="Backtest">
|
||||
<PropertyGroup>
|
||||
<CartellaDossier Condition="'$(CartellaDossier)' == ''">$(LOCALAPPDATA)\AutoBidder\Dati\Registri\Aste</CartellaDossier>
|
||||
<MaxDossier Condition="'$(MaxDossier)' == ''">0</MaxDossier>
|
||||
<Riepilogo Condition="'$(Riepilogo)' == ''">$(Radice)\bin\backtest-report.txt</Riepilogo>
|
||||
</PropertyGroup>
|
||||
|
||||
<Error Condition="!Exists('$(CartellaDossier)')"
|
||||
Text="Cartella dei dossier non trovata: $(CartellaDossier). Indicala con -p:CartellaDossier=..., oppure accendi «Dossier per ogni asta» nelle Impostazioni." />
|
||||
|
||||
<Message Importance="High" Text="== Rigiocata sui dossier ==" />
|
||||
<Message Importance="High" Text=" cartella : $(CartellaDossier)" />
|
||||
|
||||
<Exec WorkingDirectory="$(Radice)"
|
||||
Command="dotnet test "$(TestProj)" --nologo -v q --artifacts-path "$(CartellaProve)" --filter "FullyQualifiedName~RealDossierBacktest" --logger "console;verbosity=detailed""
|
||||
EnvironmentVariables="$(AmbientePulito);AUTOBIDDER_BACKTEST_DIR=$(CartellaDossier);AUTOBIDDER_BACKTEST_MAX=$(MaxDossier);AUTOBIDDER_BACKTEST_OUT=$(Riepilogo)" />
|
||||
|
||||
<Message Importance="High" Text=" riepilogo in $(Riepilogo)" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Eseguibile ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
I quattro numeri di versione arrivano da qui, non dal .csproj: le proprietà
|
||||
da riga di comando sono globali e vincono su quelle scritte nel progetto.
|
||||
Vanno passati tutti e quattro — il .csproj li assegna esplicitamente, quindi
|
||||
il solo -p:Version non ne deriverebbe nessuno. Utilities/AppInfo li legge a
|
||||
runtime, e vederne divergere uno significa un titolo finestra che mente.
|
||||
-->
|
||||
<Target Name="Pubblica" DependsOnTargets="DeterminaVersione">
|
||||
<Message Importance="High" Text="== Pubblicazione dell'eseguibile ($(V)) ==" />
|
||||
|
||||
<Exec WorkingDirectory="$(Radice)"
|
||||
EnvironmentVariables="$(AmbientePulito)"
|
||||
Command="dotnet publish "$(Csproj)" -c Release -r win-x64 --nologo -v q --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:PublishReadyToRun=true -p:PublishTrimmed=false -p:Version=$(V) -p:AssemblyVersion=$(V).0 -p:FileVersion=$(V).0 -p:InformationalVersion=$(V) -o "$(CartellaPubblicazione)"" />
|
||||
|
||||
<Error Condition="!Exists('$(CartellaPubblicazione)\AutoBidder.exe')"
|
||||
Text="Pubblicazione fallita: AutoBidder.exe non trovato." />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Pacchetto ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
L'ordine conta: si verifica, si costruisce, e il tag si crea per ultimo.
|
||||
|
||||
Il contrario sembra piu' naturale — decidi il numero, poi costruisci — ma
|
||||
lascia dietro un tag quando qualcosa va storto, e il tentativo successivo
|
||||
riparte da li'. Un giro andato male e il numero e' salito lo stesso, senza
|
||||
che sia mai esistito un pacchetto con quella versione. Taggando in fondo,
|
||||
ogni tag corrisponde a un installatore che esiste davvero.
|
||||
-->
|
||||
<Target Name="Pacchetto" DependsOnTargets="Verifica;DeterminaVersione;Pubblica">
|
||||
<Message Importance="High" Text="== Creazione dell'installatore ==" />
|
||||
|
||||
<TrovaInnoSetup>
|
||||
<Output TaskParameter="Percorso" PropertyName="Iscc" />
|
||||
</TrovaInnoSetup>
|
||||
|
||||
<MakeDir Directories="$(CartellaPacchetti)" />
|
||||
|
||||
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)"
|
||||
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||
</PropertyGroup>
|
||||
|
||||
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||
|
||||
<!-- Adesso: il pacchetto c'e', il tag puo' esistere. -->
|
||||
<CallTarget Targets="CreaTag" />
|
||||
|
||||
<Message Importance="High" Text=" " />
|
||||
<Message Importance="High" Text="Pacchetto pronto:" />
|
||||
<Message Importance="High" Text=" $(Setup)" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Versione e tag ═════════════════════ -->
|
||||
|
||||
<Target Name="DeterminaVersione">
|
||||
<!-- Nessuno dei due comandi fallisce mai: senza tag l'uscita e' vuota, e
|
||||
basta. `git describe` invece esce in errore, e qui non serve. -->
|
||||
<Exec Command="git tag --points-at HEAD" WorkingDirectory="$(Radice)"
|
||||
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="TagSuHead" />
|
||||
</Exec>
|
||||
|
||||
<Exec Command="git tag --list v*" WorkingDirectory="$(Radice)"
|
||||
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="TagEsistenti" />
|
||||
</Exec>
|
||||
|
||||
<VersioneDaTag TagsHead="$(TagSuHead)" TagsTutti="$(TagEsistenti)"
|
||||
Richiesta="$(Versione)" Csproj="$(Csproj)">
|
||||
<Output TaskParameter="Versione" PropertyName="V" />
|
||||
<Output TaskParameter="Tag" PropertyName="Tag" />
|
||||
<Output TaskParameter="DaCreare" PropertyName="TagDaCreare" />
|
||||
</VersioneDaTag>
|
||||
</Target>
|
||||
|
||||
<Target Name="CreaTag" Condition="'$(TagDaCreare)' == 'true'">
|
||||
<!-- Solo le modifiche ai file gia' tracciati: un appunto non versionato
|
||||
accanto al progetto non cambia cosa viene compilato. -->
|
||||
<Exec Command="git status --porcelain --untracked-files=no" WorkingDirectory="$(Radice)"
|
||||
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="AlberoSporco" />
|
||||
</Exec>
|
||||
|
||||
<Error Condition="'$(AlberoSporco)' != '' AND '$(ConsentiModifiche)' != 'true'"
|
||||
Text="Ci sono modifiche non committate: il tag $(Tag) indicherebbe uno stato che non e' ricostruibile.%0ACommitta prima di rilasciare, oppure rilancia con -p:ConsentiModifiche=true se sai cosa stai facendo.%0A%0A$(AlberoSporco)" />
|
||||
|
||||
<Exec Command="git tag -a $(Tag) -m "AutoBidder $(V)"" WorkingDirectory="$(Radice)"
|
||||
StandardOutputImportance="low" StandardErrorImportance="low" />
|
||||
|
||||
<Message Importance="High" Text=" tag $(Tag) creato" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Rilascio ═════════════════════ -->
|
||||
|
||||
<!-- Prima di tutto il resto: un token mancante non deve costare due minuti di
|
||||
compilazione per poi fermarsi all'ultimo passo. -->
|
||||
<Target Name="ConfigGitea">
|
||||
<LeggiConfigGitea Percorso="$(MSBuildThisFileDirectory)gitea.json">
|
||||
<Output TaskParameter="Url" PropertyName="GUrl" />
|
||||
<Output TaskParameter="Owner" PropertyName="GOwner" />
|
||||
<Output TaskParameter="Repo" PropertyName="GRepo" />
|
||||
<Output TaskParameter="Token" PropertyName="GToken" />
|
||||
</LeggiConfigGitea>
|
||||
</Target>
|
||||
|
||||
<Target Name="Rilascia" DependsOnTargets="ConfigGitea;Pacchetto">
|
||||
<Message Importance="High" Text="== Pubblicazione su Gitea ==" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Api>$(GUrl)/api/v1/repos/$(GOwner)/$(GRepo)</Api>
|
||||
<Tmp>$([System.IO.Path]::GetTempPath())AutoBidder.Rilascio</Tmp>
|
||||
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(Tmp)" />
|
||||
|
||||
<!-- Il token vive qui e solo qui, per il tempo del rilascio. -->
|
||||
<WriteLinesToFile File="$(CurlCfg)" Overwrite="true"
|
||||
Lines="header = "Authorization: token $(GToken)"" />
|
||||
|
||||
<!--
|
||||
Il tag esiste gia' in locale: l'ha creato Pacchetto, o c'era prima. Qui va
|
||||
spinto, e va verificato che ci sia arrivato.
|
||||
|
||||
Non e' una formalita'. Il push qui sotto spinge i tag, non i commit: se il
|
||||
ramo e' indietro, il tag punta a un oggetto che il remoto non conosce e il
|
||||
push viene rifiutato. A quel punto la creazione della release non
|
||||
fallisce — Gitea, non trovando il tag, lo crea da se' sulla testa del ramo
|
||||
predefinito. Verrebbe pubblicata una release che dichiara di essere il
|
||||
commit X mentre il codice allegato e' il commit Y, e nessuno se ne
|
||||
accorgerebbe. Meglio fermarsi e dire cosa manca.
|
||||
|
||||
I commit non si spingono da qui: quando spingere il ramo lo decide chi
|
||||
lavora, non la catena di rilascio.
|
||||
-->
|
||||
<Exec Command="git push --tags" WorkingDirectory="$(Radice)" ContinueOnError="true"
|
||||
StandardOutputImportance="low" StandardErrorImportance="low" />
|
||||
|
||||
<Exec Command="git ls-remote --tags origin refs/tags/$(Tag)" WorkingDirectory="$(Radice)"
|
||||
ConsoleToMSBuild="true" ContinueOnError="true" StandardOutputImportance="low">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="TagSulRemoto" />
|
||||
</Exec>
|
||||
|
||||
<Error Condition="'$(TagSulRemoto)' == ''"
|
||||
Text="Il tag $(Tag) non e' arrivato sul remoto, quasi sempre perche' il ramo e' indietro.%0AGitea creerebbe la release sulla testa del ramo predefinito: il codice allegato non corrisponderebbe al commit dichiarato.%0A%0ASpingi il ramo e rilancia: git push" />
|
||||
|
||||
<Message Importance="High" Text=" tag $(Tag) sul remoto" />
|
||||
|
||||
<!-- Release gia' presente? -->
|
||||
<Exec EchoOff="true" ContinueOnError="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -o "$(RispostaJson)" "$(Api)/releases/tags/$(Tag)"" />
|
||||
|
||||
<LeggiIdRelease Risposta="$(RispostaJson)">
|
||||
<Output TaskParameter="Id" PropertyName="IdEsistente" />
|
||||
</LeggiIdRelease>
|
||||
|
||||
<Error Condition="'$(IdEsistente)' != '' AND '$(Sovrascrivi)' != 'true'"
|
||||
Text="La release $(Tag) esiste gia'. Alza la versione, oppure rilancia con -p:Sovrascrivi=true." />
|
||||
|
||||
<Exec Condition="'$(IdEsistente)' != ''" EchoOff="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -X DELETE "$(Api)/releases/$(IdEsistente)"" />
|
||||
<Message Condition="'$(IdEsistente)' != ''" Importance="High"
|
||||
Text=" release $(Tag) esistente: sostituita" />
|
||||
|
||||
<!-- Creazione -->
|
||||
<PreparaCorpoRelease Destinazione="$(CorpoJson)" Tag="$(Tag)" Versione="$(V)"
|
||||
Note="$(Note)" Bozza="$(Bozza)" />
|
||||
|
||||
<Exec EchoOff="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -X POST -H "Content-Type: application/json" --data-binary "@$(CorpoJson)" -o "$(RispostaJson)" "$(Api)/releases"" />
|
||||
|
||||
<LeggiIdRelease Risposta="$(RispostaJson)">
|
||||
<Output TaskParameter="Id" PropertyName="IdRelease" />
|
||||
<Output TaskParameter="Errore" PropertyName="ErroreRelease" />
|
||||
</LeggiIdRelease>
|
||||
|
||||
<Error Condition="'$(IdRelease)' == ''"
|
||||
Text="Creazione della release non riuscita: $(ErroreRelease)" />
|
||||
|
||||
<Message Importance="High" Text=" release creata" />
|
||||
|
||||
<!-- Allegati: sia l'installatore sia l'eseguibile nudo. Chi non vuole
|
||||
installare niente deve continuare a poter scaricare il solo .exe. -->
|
||||
<ItemGroup>
|
||||
<Allegato Include="$(Setup)" />
|
||||
<Allegato Include="$(CartellaPubblicazione)\AutoBidder.exe" />
|
||||
</ItemGroup>
|
||||
|
||||
<Exec Condition="Exists('%(Allegato.FullPath)')" EchoOff="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -X POST -F "attachment=@%(Allegato.FullPath)" "$(Api)/releases/$(IdRelease)/assets?name=%(Allegato.Filename)%(Allegato.Extension)"" />
|
||||
|
||||
<Message Importance="High" Text=" caricato %(Allegato.Filename)%(Allegato.Extension)"
|
||||
Condition="Exists('%(Allegato.FullPath)')" />
|
||||
|
||||
<!-- Il token non deve sopravvivere al rilascio. -->
|
||||
<Delete Files="$(CurlCfg);$(CorpoJson);$(RispostaJson)" ContinueOnError="true" />
|
||||
|
||||
<Message Importance="High" Text=" " />
|
||||
<Message Importance="High" Text="Rilascio completato:" />
|
||||
<Message Importance="High" Text=" $(GUrl)/$(GOwner)/$(GRepo)/releases/tag/$(Tag)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"_commento": "Copia questo file in gitea.json e metti il token. gitea.json e' escluso dal controllo di versione perche' contiene una credenziale. In alternativa usa le variabili d'ambiente GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN, che hanno la precedenza su questo file.",
|
||||
|
||||
"url": "http://192.168.30.23:3000",
|
||||
"owner": "Alby96",
|
||||
"repo": "Mimante",
|
||||
|
||||
"_token": "Gitea > Impostazioni > Applicazioni > Genera nuovo token, permesso 'repository: read and write'.",
|
||||
"token": "INSERISCI_QUI_IL_TOKEN"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# Rigioca i dossier delle aste concluse e stampa quanto costerebbe ciascun anticipo.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File .\backtest.ps1
|
||||
# powershell -ExecutionPolicy Bypass -File .\backtest.ps1 -Max 200
|
||||
#
|
||||
# Il motore punta solo nei cicli che arrivano fino all'anticipo senza che nessun altro
|
||||
# abbia puntato: la rigiocata conta quei cicli, quindi dice quante puntate si
|
||||
# spenderebbero e quante ne bloccherebbero le strategie.
|
||||
#
|
||||
# NON dice se avresti vinto: una nostra puntata rimette in gioco l'asta, e la reazione
|
||||
# degli avversari non e' registrata da nessuna parte.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Cartella dei dossier. Vuota = quella in uso dall'applicazione.
|
||||
[string] $Cartella = "",
|
||||
|
||||
# Quanti dossier al massimo (0 = tutti). I file sono grossi: per un giro veloce, 200.
|
||||
[int] $Max = 0,
|
||||
|
||||
# Dove scrivere il riepilogo.
|
||||
[string] $Report = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
if (-not $Cartella) {
|
||||
# Stessa struttura di Utilities/AppPaths: Dati\Registri\Aste sotto %LocalAppData%.
|
||||
$Cartella = Join-Path $env:LOCALAPPDATA "AutoBidder\Dati\Registri\Aste"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Cartella)) {
|
||||
Write-Host "Cartella dei dossier non trovata: $Cartella" -ForegroundColor Red
|
||||
Write-Host "Indicala con -Cartella, oppure accendi 'Dossier per ogni asta' nelle Impostazioni." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$conteggio = (Get-ChildItem -Path $Cartella -Filter *.jsonl -File | Measure-Object).Count
|
||||
if ($conteggio -eq 0) {
|
||||
Write-Host "Nessun dossier (*.jsonl) in $Cartella" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $Report) {
|
||||
$Report = Join-Path $PSScriptRoot "backtest-report.txt"
|
||||
}
|
||||
|
||||
Write-Host "== Rigiocata sui dossier ==" -ForegroundColor Cyan
|
||||
Write-Host " cartella : $Cartella"
|
||||
Write-Host " dossier : $conteggio$(if ($Max -gt 0) { " (ne verranno letti $Max)" })"
|
||||
Write-Host " riepilogo: $Report"
|
||||
Write-Host ""
|
||||
|
||||
$env:AUTOBIDDER_BACKTEST_DIR = $Cartella
|
||||
$env:AUTOBIDDER_BACKTEST_MAX = "$Max"
|
||||
$env:AUTOBIDDER_BACKTEST_OUT = $Report
|
||||
|
||||
# L'applicazione potrebbe essere aperta e tenere bloccato AutoBidder.exe: si compila
|
||||
# in una cartella a parte, come fa la verifica.
|
||||
$out = Join-Path ([System.IO.Path]::GetTempPath()) "AutoBidder.Backtest"
|
||||
|
||||
& dotnet test Tests\AutoBidder.Tests.csproj --nologo -v q -o $out `
|
||||
--filter "FullyQualifiedName~RealDossierBacktest" `
|
||||
--logger "console;verbosity=detailed" 2>&1 |
|
||||
Where-Object { $_ -notmatch '^\s*$' } |
|
||||
ForEach-Object { Write-Host $_ }
|
||||
|
||||
$esito = $LASTEXITCODE
|
||||
|
||||
Write-Host ""
|
||||
if (Test-Path $Report) {
|
||||
Write-Host "== Riepilogo ==" -ForegroundColor Cyan
|
||||
Get-Content $Report | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
|
||||
if ($esito -ne 0) {
|
||||
Write-Host "`nLa rigiocata ha trovato dei problemi (vedi sopra)." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`nRigiocata completata." -ForegroundColor Green
|
||||
exit 0
|
||||
@@ -0,0 +1,36 @@
|
||||
<#
|
||||
pubblica.ps1 - Crea AutoBidder come UNICO file .exe self-contained per Windows x64.
|
||||
|
||||
Uso:
|
||||
powershell -ExecutionPolicy Bypass -File .\pubblica.ps1
|
||||
|
||||
Risultato:
|
||||
bin\publish\win-x64\AutoBidder.exe (doppio click per avviare, nessuna installazione)
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
Set-Location $here
|
||||
|
||||
Write-Host "==> Pubblicazione AutoBidder (self-contained, single-file, win-x64)..." -ForegroundColor Cyan
|
||||
|
||||
dotnet publish "AutoBidder.csproj" -c Release -r win-x64 `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:EnableCompressionInSingleFile=true `
|
||||
-p:PublishReadyToRun=true `
|
||||
-p:PublishTrimmed=false `
|
||||
-o "bin\publish\win-x64"
|
||||
|
||||
$exe = Join-Path $here "bin\publish\win-x64\AutoBidder.exe"
|
||||
if (Test-Path $exe) {
|
||||
$sizeMB = [math]::Round((Get-Item $exe).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "==> FATTO. Eseguibile pronto:" -ForegroundColor Green
|
||||
Write-Host " $exe ($sizeMB MB)" -ForegroundColor Green
|
||||
Write-Host " Fai doppio click per avviare. Nessun software da installare." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "==> ERRORE: eseguibile non trovato." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<#
|
||||
rilascia.ps1 — dalla sorgente al pacchetto pubblicato su Gitea, in un comando.
|
||||
|
||||
Passi:
|
||||
1. verifica.ps1 (compila + test) — saltabile con -SaltaVerifica
|
||||
2. pubblica l'eseguibile self-contained
|
||||
3. compila l'installatore con Inno Setup
|
||||
4. crea il tag, la release su Gitea e carica i file
|
||||
|
||||
Uso:
|
||||
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1 -SoloPacchetto
|
||||
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1 -Note "Cosa cambia"
|
||||
|
||||
Configurazione di Gitea (una volta sola): copia gitea.example.json in
|
||||
gitea.json e riempilo. Il file è escluso dal controllo di versione perché
|
||||
contiene un token. In alternativa, le variabili d'ambiente
|
||||
GITEA_URL / GITEA_OWNER / GITEA_REPO / GITEA_TOKEN hanno la precedenza.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Ferma dopo aver creato l'installatore, senza toccare Gitea.
|
||||
[switch] $SoloPacchetto,
|
||||
|
||||
# Salta compilazione e test. Da usare solo se li hai appena eseguiti.
|
||||
[switch] $SaltaVerifica,
|
||||
|
||||
# Testo delle note di rilascio. Vuoto = generate dai commit dall'ultimo tag.
|
||||
[string] $Note = "",
|
||||
|
||||
# Contrassegna la release come bozza o come anteprima.
|
||||
[switch] $Bozza,
|
||||
[switch] $Anteprima,
|
||||
|
||||
# Sovrascrive una release con lo stesso tag invece di fermarsi.
|
||||
[switch] $Sovrascrivi
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
function Titolo($t) { Write-Host "`n== $t ==" -ForegroundColor Cyan }
|
||||
function Ok($t) { Write-Host " $t" -ForegroundColor Green }
|
||||
function Nota($t) { Write-Host " $t" -ForegroundColor DarkGray }
|
||||
function Errore($t) { Write-Host "`n$t" -ForegroundColor Red }
|
||||
|
||||
# ── 0. Versione ──────────────────────────────────────────────────────────────
|
||||
# La versione ha una sola origine: il .csproj. Ripeterla qui vorrebbe dire
|
||||
# vederle divergere al primo rilascio fatto di fretta.
|
||||
[xml] $csproj = Get-Content .\AutoBidder.csproj
|
||||
$versione = ($csproj.Project.PropertyGroup | Where-Object { $_.Version } | Select-Object -First 1).Version
|
||||
|
||||
if (-not $versione) {
|
||||
Errore "Versione non trovata in AutoBidder.csproj (<Version>)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tag = "v$versione"
|
||||
Titolo "AutoBidder $versione"
|
||||
|
||||
# ── 1. Verifica ──────────────────────────────────────────────────────────────
|
||||
if ($SaltaVerifica) {
|
||||
Nota "verifica saltata su richiesta"
|
||||
} else {
|
||||
Titolo "Verifica (compilazione + test)"
|
||||
& powershell -ExecutionPolicy Bypass -File .\verifica.ps1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Errore "Verifica fallita: niente da rilasciare."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ── 2. Eseguibile ────────────────────────────────────────────────────────────
|
||||
Titolo "Pubblicazione dell'eseguibile"
|
||||
|
||||
$pubDir = Join-Path $PSScriptRoot "bin\publish\win-x64"
|
||||
$exe = Join-Path $pubDir "AutoBidder.exe"
|
||||
|
||||
dotnet publish .\AutoBidder.csproj -c Release -r win-x64 --nologo -v q `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:EnableCompressionInSingleFile=true `
|
||||
-p:PublishReadyToRun=true `
|
||||
-p:PublishTrimmed=false `
|
||||
-o $pubDir
|
||||
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $exe)) {
|
||||
Errore "Pubblicazione fallita."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Ok ("AutoBidder.exe ({0:N1} MB)" -f ((Get-Item $exe).Length / 1MB))
|
||||
|
||||
# La versione scritta nell'eseguibile deve corrispondere: se qualcuno cambia il
|
||||
# .csproj senza ricompilare, il pacchetto porterebbe un numero e il programma un altro.
|
||||
$versioneExe = (Get-Item $exe).VersionInfo.ProductVersion
|
||||
if ($versioneExe -and -not $versioneExe.StartsWith($versione)) {
|
||||
Errore "L'eseguibile dichiara $versioneExe ma il progetto dice $versione."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── 3. Installatore ──────────────────────────────────────────────────────────
|
||||
Titolo "Creazione dell'installatore"
|
||||
|
||||
$iscc = @(
|
||||
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
|
||||
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
||||
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
||||
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $iscc) {
|
||||
Errore "Inno Setup 6 non trovato."
|
||||
Write-Host "Installalo con: winget install -e --id JRSoftware.InnoSetup" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$outDir = Join-Path $PSScriptRoot "bin\installer"
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
|
||||
& $iscc /Qp `
|
||||
"/DAppVersion=$versione" `
|
||||
"/DSourceExe=$exe" `
|
||||
"/DOutputDir=$outDir" `
|
||||
".\installer\AutoBidder.iss"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Errore "Inno Setup ha restituito un errore."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$setup = Join-Path $outDir "AutoBidder-$versione-setup.exe"
|
||||
if (-not (Test-Path $setup)) {
|
||||
Errore "Installatore non trovato: $setup"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Ok ("{0} ({1:N1} MB)" -f (Split-Path $setup -Leaf), ((Get-Item $setup).Length / 1MB))
|
||||
|
||||
if ($SoloPacchetto) {
|
||||
Write-Host "`nPacchetto pronto in $outDir" -ForegroundColor Green
|
||||
Write-Host "Gitea non è stato toccato (-SoloPacchetto)." -ForegroundColor DarkGray
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── 4. Gitea ─────────────────────────────────────────────────────────────────
|
||||
Titolo "Pubblicazione su Gitea"
|
||||
|
||||
$cfgFile = Join-Path $PSScriptRoot "gitea.json"
|
||||
$cfg = if (Test-Path $cfgFile) { Get-Content $cfgFile -Raw | ConvertFrom-Json } else { $null }
|
||||
|
||||
function Impostazione($nomeEnv, $campo) {
|
||||
$v = [Environment]::GetEnvironmentVariable($nomeEnv)
|
||||
if ($v) { return $v }
|
||||
if ($cfg -and $cfg.PSObject.Properties.Name -contains $campo) { return $cfg.$campo }
|
||||
return $null
|
||||
}
|
||||
|
||||
$url = Impostazione 'GITEA_URL' 'url'
|
||||
$owner = Impostazione 'GITEA_OWNER' 'owner'
|
||||
$repo = Impostazione 'GITEA_REPO' 'repo'
|
||||
$token = Impostazione 'GITEA_TOKEN' 'token'
|
||||
|
||||
if (-not $url -or -not $owner -or -not $repo -or -not $token) {
|
||||
Errore "Configurazione di Gitea incompleta."
|
||||
Write-Host @"
|
||||
Servono quattro valori: url, owner, repo, token.
|
||||
|
||||
1. copia gitea.example.json in gitea.json e riempilo, oppure
|
||||
2. imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.
|
||||
|
||||
Il token si crea in Gitea da Impostazioni > Applicazioni > Genera nuovo token,
|
||||
con il permesso 'repository: read and write'.
|
||||
|
||||
L'installatore è comunque pronto in:
|
||||
$setup
|
||||
"@ -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$url = $url.TrimEnd('/')
|
||||
$api = "$url/api/v1/repos/$owner/$repo"
|
||||
$headers = @{ Authorization = "token $token" }
|
||||
|
||||
Nota "$url/$owner/$repo tag $tag"
|
||||
|
||||
# Note di rilascio: se non le detta l'utente, si prendono dai commit.
|
||||
if (-not $Note) {
|
||||
$ultimo = & git describe --tags --abbrev=0 2>$null
|
||||
$intervallo = if ($LASTEXITCODE -eq 0 -and $ultimo) { "$ultimo..HEAD" } else { "HEAD" }
|
||||
$commit = & git log $intervallo --pretty=format:"- %s" --no-merges 2>$null
|
||||
|
||||
$Note = if ($commit) { ($commit | Out-String).Trim() } else { "Versione $versione." }
|
||||
}
|
||||
|
||||
# Il tag deve esistere sul server: la release ci si appoggia.
|
||||
$rami = & git rev-parse --git-dir 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$esiste = & git tag -l $tag
|
||||
if (-not $esiste) {
|
||||
& git tag -a $tag -m "AutoBidder $versione"
|
||||
Ok "tag $tag creato"
|
||||
}
|
||||
|
||||
$remoto = & git remote 2>$null | Select-Object -First 1
|
||||
if ($remoto) {
|
||||
& git push $remoto $tag 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { Ok "tag inviato a $remoto" }
|
||||
else { Nota "tag non inviato (probabilmente c'era già)" }
|
||||
} else {
|
||||
Nota "nessun remoto git configurato: la release userà il tag come nome"
|
||||
}
|
||||
}
|
||||
|
||||
# Release già presente?
|
||||
$esistente = $null
|
||||
try {
|
||||
$esistente = Invoke-RestMethod -Uri "$api/releases/tags/$tag" -Headers $headers -Method Get
|
||||
} catch {
|
||||
# 404 = non c'è, ed è il caso normale.
|
||||
}
|
||||
|
||||
if ($esistente) {
|
||||
if (-not $Sovrascrivi) {
|
||||
Errore "La release $tag esiste già su Gitea."
|
||||
Write-Host "Alza <Version> in AutoBidder.csproj, oppure rilancia con -Sovrascrivi." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Nota "release $tag esistente: verrà sostituita"
|
||||
Invoke-RestMethod -Uri "$api/releases/$($esistente.id)" -Headers $headers -Method Delete | Out-Null
|
||||
}
|
||||
|
||||
$corpo = @{
|
||||
tag_name = $tag
|
||||
name = "AutoBidder $versione"
|
||||
body = $Note
|
||||
draft = [bool]$Bozza
|
||||
prerelease = [bool]$Anteprima
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$release = Invoke-RestMethod -Uri "$api/releases" -Headers $headers -Method Post `
|
||||
-ContentType 'application/json' -Body $corpo
|
||||
} catch {
|
||||
Errore "Creazione della release non riuscita: $($_.Exception.Message)"
|
||||
if ($_.ErrorDetails.Message) { Write-Host $_.ErrorDetails.Message -ForegroundColor DarkGray }
|
||||
Write-Host "`nL'installatore resta disponibile in:`n $setup" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Ok "release creata"
|
||||
|
||||
# ── 5. File allegati ─────────────────────────────────────────────────────────
|
||||
# Si carica sia l'installatore sia l'eseguibile nudo: chi non vuole installare
|
||||
# nulla continua a poter scaricare il solo .exe, che è come l'app è nata.
|
||||
function Carica($percorso) {
|
||||
$nome = Split-Path $percorso -Leaf
|
||||
$uri = "$api/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($nome))"
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Uri $uri -Headers $headers -Method Post -Form @{
|
||||
attachment = Get-Item $percorso
|
||||
} | Out-Null
|
||||
Ok ("caricato {0} ({1:N1} MB)" -f $nome, ((Get-Item $percorso).Length / 1MB))
|
||||
return $true
|
||||
} catch {
|
||||
Errore "Caricamento di $nome non riuscito: $($_.Exception.Message)"
|
||||
if ($_.ErrorDetails.Message) { Write-Host $_.ErrorDetails.Message -ForegroundColor DarkGray }
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$tuttoOk = (Carica $setup) -and (Carica $exe)
|
||||
|
||||
if (-not $tuttoOk) {
|
||||
Errore "Release creata ma senza tutti i file. Caricali a mano da $url/$owner/$repo/releases"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`nRilascio completato." -ForegroundColor Green
|
||||
Write-Host " $url/$owner/$repo/releases/tag/$tag" -ForegroundColor Green
|
||||
exit 0
|
||||
@@ -0,0 +1,40 @@
|
||||
# Verifica rapida: compila tutto e lancia i test.
|
||||
# Da eseguire dopo ogni modifica, prima di avviare l'applicazione.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File .\verifica.ps1
|
||||
#
|
||||
# Restituisce 0 se tutto è a posto, 1 altrimenti: utilizzabile in un hook.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
Write-Host "== Compilazione ==" -ForegroundColor Cyan
|
||||
$build = & dotnet build AutoBidder.sln --nologo -v q 2>&1
|
||||
$buildOk = $LASTEXITCODE -eq 0
|
||||
|
||||
$problemi = $build | Select-String -Pattern 'error|warning CS'
|
||||
if ($problemi) { $problemi | ForEach-Object { Write-Host $_.Line -ForegroundColor Yellow } }
|
||||
|
||||
if (-not $buildOk) {
|
||||
Write-Host "COMPILAZIONE FALLITA" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "compilazione ok, 0 errori" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n== Test ==" -ForegroundColor Cyan
|
||||
$test = & dotnet test Tests\AutoBidder.Tests.csproj --nologo -v q --no-build 2>&1
|
||||
$testOk = $LASTEXITCODE -eq 0
|
||||
|
||||
$riga = $test | Select-String -Pattern 'Superato!|Non superato|Failed!|Passed!'
|
||||
if ($riga) { $riga | ForEach-Object { Write-Host $_.Line } }
|
||||
|
||||
if (-not $testOk) {
|
||||
# In caso di rosso servono i dettagli, non il riassunto.
|
||||
$test | Select-String -Pattern 'Assert|Error Message|Stack Trace|\[FAIL\]' |
|
||||
Select-Object -First 30 | ForEach-Object { Write-Host $_.Line -ForegroundColor Red }
|
||||
Write-Host "`nTEST FALLITI" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`nTutto a posto." -ForegroundColor Green
|
||||
exit 0
|
||||
Reference in New Issue
Block a user