Fixes, Windows Update (step 12), auto-reboot, PS window hide
- Write-Log creates C:\Windows\Setup\Scripts\ automatically (was failing on fresh install) - Step 12: PSWindowsUpdate first pass + X9-WindowsUpdate scheduled task for post-reboot rounds (handles typical 2-3 reboot cycles on fresh Windows, task self-deletes when up to date) - GUI summary: 60s countdown auto-reboot with "Restartovat ted" / "Zrusit restart" buttons - runner: HideWindow=true prevents PS console from appearing over GUI - runner: skipPSNoiseLine filters PS error metadata (CategoryInfo, FullyQualifiedErrorId etc.) - web: fix curl command to include https:// prefix
This commit is contained in:
parent
e62cbaaec3
commit
7e6095d1bd
20 changed files with 247 additions and 5 deletions
|
|
@ -77,6 +77,7 @@ func DefaultConfig() Config {
|
|||
"backinfo": true,
|
||||
"activation": true,
|
||||
"dellUpdate": true,
|
||||
"windowsUpdate": true,
|
||||
"network": true,
|
||||
"pcIdentity": true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -324,11 +325,16 @@ func runPhase(runCfg runner.RunConfig, steps []runner.Step) []runner.Result {
|
|||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Phase 3 – Summary
|
||||
// Phase 3 – Summary with auto-reboot countdown
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
const rebootCountdown = 60 // seconds before automatic reboot
|
||||
|
||||
func donePhase(results []runner.Result) {
|
||||
var mw *walk.MainWindow
|
||||
var (
|
||||
mw *walk.MainWindow
|
||||
countdownLbl *walk.Label
|
||||
)
|
||||
|
||||
ok, errs, skipped := 0, 0, 0
|
||||
rows := make([]Widget, 0, len(results))
|
||||
|
|
@ -358,6 +364,9 @@ func donePhase(results []runner.Result) {
|
|||
|
||||
summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped)
|
||||
|
||||
// cancelled by "Zrusit restart" button
|
||||
cancelReboot := make(chan struct{})
|
||||
|
||||
if err := (MainWindow{
|
||||
AssignTo: &mw,
|
||||
Title: "xetup \u2014 hotovo",
|
||||
|
|
@ -371,7 +380,7 @@ func donePhase(results []runner.Result) {
|
|||
},
|
||||
HSeparator{},
|
||||
ScrollView{
|
||||
MinSize: Size{Height: 560},
|
||||
MinSize: Size{Height: 510},
|
||||
Layout: VBox{MarginsZero: true},
|
||||
Children: rows,
|
||||
},
|
||||
|
|
@ -381,16 +390,34 @@ func donePhase(results []runner.Result) {
|
|||
Alignment: AlignHCenterVNear,
|
||||
Font: Font{Bold: true},
|
||||
},
|
||||
Label{
|
||||
AssignTo: &countdownLbl,
|
||||
Text: fmt.Sprintf("Restart za %ds...", rebootCountdown),
|
||||
Alignment: AlignHCenterVNear,
|
||||
},
|
||||
Composite{
|
||||
Layout: HBox{MarginsZero: true},
|
||||
Children: []Widget{
|
||||
HSpacer{},
|
||||
PushButton{
|
||||
Text: " ZAVRIT ",
|
||||
Text: " Restartovat ted ",
|
||||
OnClicked: func() {
|
||||
close(cancelReboot) // signal goroutine to stop ticker
|
||||
reboot()
|
||||
mw.Close()
|
||||
},
|
||||
},
|
||||
PushButton{
|
||||
Text: " Zrusit restart ",
|
||||
OnClicked: func() {
|
||||
select {
|
||||
case <-cancelReboot: // already closed
|
||||
default:
|
||||
close(cancelReboot)
|
||||
}
|
||||
countdownLbl.SetText("Restart zrusen.")
|
||||
},
|
||||
},
|
||||
HSpacer{},
|
||||
},
|
||||
},
|
||||
|
|
@ -399,9 +426,42 @@ func donePhase(results []runner.Result) {
|
|||
return
|
||||
}
|
||||
|
||||
// Countdown goroutine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
remaining := rebootCountdown
|
||||
for {
|
||||
select {
|
||||
case <-cancelReboot:
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining--
|
||||
r := remaining
|
||||
mw.Synchronize(func() {
|
||||
if r > 0 {
|
||||
countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
|
||||
} else {
|
||||
countdownLbl.SetText("Restartuji...")
|
||||
}
|
||||
})
|
||||
if remaining <= 0 {
|
||||
reboot()
|
||||
mw.Synchronize(func() { mw.Close() })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
mw.Run()
|
||||
}
|
||||
|
||||
// reboot issues an immediate Windows restart.
|
||||
func reboot() {
|
||||
exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
|
|
|||
7
internal/runner/hidecmd_other.go
Normal file
7
internal/runner/hidecmd_other.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !windows
|
||||
|
||||
package runner
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func hideWindow(cmd *exec.Cmd) {}
|
||||
14
internal/runner/hidecmd_windows.go
Normal file
14
internal/runner/hidecmd_windows.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//go:build windows
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// hideWindow prevents the child process from creating a visible console window.
|
||||
// Without this, powershell.exe opens a full-size window on top of the GUI.
|
||||
func hideWindow(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ func AllSteps() []Step {
|
|||
{ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"},
|
||||
{ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"},
|
||||
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
|
||||
{ID: "windowsUpdate", Num: "12", Name: "Windows Update", ScriptName: "12-windows-update.ps1"},
|
||||
{ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.ps1"},
|
||||
{ID: "pcIdentity", Num: "10", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
|
||||
}
|
||||
|
|
@ -222,6 +223,7 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
|
|||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "powershell.exe", args...)
|
||||
hideWindow(cmd) // prevent PS console window from appearing over the GUI
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
|
|
@ -236,6 +238,9 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
|
|||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if skipPSNoiseLine(line) {
|
||||
continue
|
||||
}
|
||||
r.onLog(LogLine{
|
||||
StepID: step.ID,
|
||||
Text: line,
|
||||
|
|
@ -246,6 +251,33 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
|
|||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// skipPSNoiseLine returns true for PowerShell stderr noise that clutters the log:
|
||||
// multi-line error blocks (At line:N, CategoryInfo, FullyQualifiedErrorId, etc.),
|
||||
// blank lines, and VERBOSE: prefix lines already handled by Write-Log.
|
||||
func skipPSNoiseLine(line string) bool {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range []string{
|
||||
"At line:",
|
||||
"+ CategoryInfo",
|
||||
"+ FullyQualifiedErrorId",
|
||||
"+ PositionMessage",
|
||||
"VERBOSE:",
|
||||
"DEBUG:",
|
||||
} {
|
||||
if strings.HasPrefix(trimmed, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// PS error continuation lines start with spaces + "+" or "~"
|
||||
if len(trimmed) > 0 && (trimmed[0] == '+' || strings.HasPrefix(trimmed, "~")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
|
||||
func parseLevel(line string) string {
|
||||
if strings.Contains(line, "] [OK]") {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
switch ($Level) {
|
||||
"OK" { Write-Host $line -ForegroundColor Green }
|
||||
|
|
|
|||
115
scripts/12-windows-update.ps1
Normal file
115
scripts/12-windows-update.ps1
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Installs all available Windows Updates via PSWindowsUpdate module.
|
||||
|
||||
.DESCRIPTION
|
||||
First pass: installs all currently available updates without rebooting.
|
||||
Then registers a scheduled task "X9-WindowsUpdate" that runs on every
|
||||
logon (as SYSTEM) until no more updates are found - handles the typical
|
||||
2-3 reboot cycles required on a fresh Windows installation.
|
||||
|
||||
Operator workflow:
|
||||
1. xetup completes all steps
|
||||
2. Operator reboots manually
|
||||
3. On each subsequent logon the scheduled task runs another update pass
|
||||
4. Task removes itself automatically when system is fully up to date
|
||||
|
||||
.ITEMS
|
||||
nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
|
||||
spustit-prvni-kolo-windows-update: First update pass without reboot - installs all currently available updates.
|
||||
registrovat-scheduled-task-pro-dalsi-kola: Registers X9-WindowsUpdate scheduled task that runs on logon, handles post-reboot update rounds, and self-deletes when no more updates are found.
|
||||
#>
|
||||
param(
|
||||
[object]$Config,
|
||||
[string]$LogFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
Write-Output $line
|
||||
}
|
||||
|
||||
Write-Log "=== Step 12 - Windows Update ===" -Level STEP
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1. NuGet provider + PSWindowsUpdate module
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
|
||||
try {
|
||||
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
|
||||
$existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
|
||||
if ($existing) {
|
||||
Write-Log " PSWindowsUpdate $($existing.Version) already installed" -Level INFO
|
||||
} else {
|
||||
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
|
||||
Write-Log " PSWindowsUpdate installed" -Level OK
|
||||
}
|
||||
Import-Module PSWindowsUpdate -Force
|
||||
} catch {
|
||||
Write-Log " Module setup failed: $_" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 2. First update pass (no reboot)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Running first Windows Update pass..." -Level INFO
|
||||
try {
|
||||
$result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
|
||||
$installed = @($result | Where-Object { $_ -match 'KB\d+|Downloaded|Installed' })
|
||||
if ($installed.Count -gt 0) {
|
||||
$result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log " $_" -Level INFO }
|
||||
Write-Log " First pass complete - reboot required for remaining rounds" -Level OK
|
||||
} else {
|
||||
Write-Log " System already up to date" -Level OK
|
||||
}
|
||||
} catch {
|
||||
Write-Log " First pass failed: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 3. Scheduled task for post-reboot update rounds (self-deleting)
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Registering post-reboot update task..." -Level INFO
|
||||
|
||||
$taskName = "X9-WindowsUpdate"
|
||||
|
||||
# PowerShell block that runs on each logon until no more updates found
|
||||
$updateScript = @'
|
||||
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
|
||||
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
|
||||
if ($updates) {
|
||||
Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
|
||||
} else {
|
||||
# No more updates - remove this task
|
||||
Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
|
||||
}
|
||||
'@
|
||||
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
|
||||
-MultipleInstances IgnoreNew
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
|
||||
# Remove existing task first (idempotent)
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
||||
-Settings $settings -Principal $principal -Force | Out-Null
|
||||
|
||||
Write-Log " Task '$taskName' registered - runs on each logon until fully updated" -Level OK
|
||||
} catch {
|
||||
Write-Log " Failed to register scheduled task: $_" -Level WARN
|
||||
Write-Log " Manual Windows Update rounds will be needed after reboot" -Level WARN
|
||||
}
|
||||
|
||||
Write-Log "Step 12 - Windows Update complete" -Level OK
|
||||
Write-Log " ACTION REQUIRED: Reboot the machine to complete remaining update rounds" -Level WARN
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
</div>
|
||||
|
||||
<div class="install-box">
|
||||
<code id="install-cmd">curl -Lo xetup.exe xetup.x9.cz/dl</code>
|
||||
<code id="install-cmd">curl -Lo xetup.exe https://xetup.x9.cz/dl</code>
|
||||
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue