mirror of https://github.com/hrfee/jfa-go
If enabled, jfa-go pings buildrone (hosted at builds.hrfee.pw) every 30 min for new updates. If there is one, it gets information (and if applicable, a binary) from the appropriate source (buildrone, github, or dockerhub) and displays it on the admin page. You can switch update channels between stable and unstable. For binary releases, updates are downloaded automatically and installed when the user presses update. Since this obviously introduces some "phone-home" functionality into jfa-go, I just want to say IPs are not and will not be logged by buildrone, although I may later introduce functionality to give a rough idea of the number of users (again, no IPs stored). The whole thing can also be turned off in settings.pull/75/head
parent
9787fce275
commit
92332206f0
@ -1,27 +0,0 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
version = sys.argv[1].replace('v', '')
|
||||
except IndexError:
|
||||
version = "git"
|
||||
|
||||
if version == "auto":
|
||||
try:
|
||||
version = subprocess.check_output("git describe --exact-match HEAD".split()).decode("utf-8").rstrip().replace('v', '')
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode == 128:
|
||||
version = "git"
|
||||
|
||||
commit = subprocess.check_output("git rev-parse --short HEAD".split()).decode("utf-8").rstrip()
|
||||
|
||||
file = f'package main; const VERSION = "{version}"; const COMMIT = "{commit}";'
|
||||
|
||||
try:
|
||||
writeto = sys.argv[2]
|
||||
except IndexError:
|
||||
writeto = "version.go"
|
||||
|
||||
with open(writeto, 'w') as f:
|
||||
f.write(file)
|
||||
|
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
|
||||
VERSION="$(echo $VERSION | sed 's/v//g')" $@
|
@ -0,0 +1,124 @@
|
||||
import { _get, _post, toggleLoader } from "../modules/common.js";
|
||||
import { Marked, Renderer } from "@ts-stack/markdown";
|
||||
|
||||
interface updateDTO {
|
||||
new: boolean;
|
||||
update: Update;
|
||||
}
|
||||
|
||||
export class Updater implements updater {
|
||||
private _update: Update;
|
||||
private _date: Date;
|
||||
updateAvailable = false;
|
||||
|
||||
checkForUpdates = (run?: (req: XMLHttpRequest) => void) => _get("/config/update", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate"));
|
||||
return
|
||||
}
|
||||
let resp = req.response as updateDTO;
|
||||
if (resp.new) {
|
||||
this.update = resp.update;
|
||||
if (run) { run(req); }
|
||||
// } else {
|
||||
// window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable"));
|
||||
}
|
||||
}
|
||||
});
|
||||
get date(): number { return Math.floor(this._date.getTime() / 1000); }
|
||||
set date(unix: number) {
|
||||
this._date = new Date(unix * 1000);
|
||||
document.getElementById("update-date").textContent = this._date.toDateString() + " " + this._date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
get description(): string { return this._update.description; }
|
||||
set description(description: string) {
|
||||
this._update.description = description;
|
||||
const el = document.getElementById("update-description") as HTMLParagraphElement;
|
||||
el.textContent = description;
|
||||
if (this.version == "git") {
|
||||
el.classList.add("monospace");
|
||||
} else {
|
||||
el.classList.remove("monospace");
|
||||
}
|
||||
}
|
||||
|
||||
get changelog(): string { return this._update.changelog; }
|
||||
set changelog(changelog: string) {
|
||||
this._update.changelog = changelog;
|
||||
|
||||
document.getElementById("update-changelog").innerHTML = Marked.parse(changelog);
|
||||
}
|
||||
|
||||
get version(): string { return this._update.version; }
|
||||
set version(version: string) {
|
||||
this._update.version = version;
|
||||
document.getElementById("update-version").textContent = version;
|
||||
}
|
||||
|
||||
get commit(): string { return this._update.commit; }
|
||||
set commit(commit: string) {
|
||||
this._update.commit = commit;
|
||||
document.getElementById("update-commit").textContent = commit.slice(0, 7);
|
||||
}
|
||||
|
||||
get link(): string { return this._update.link; }
|
||||
set link(link: string) {
|
||||
this._update.link = link;
|
||||
(document.getElementById("update-version") as HTMLAnchorElement).href = link;
|
||||
}
|
||||
|
||||
get download_link(): string { return this._update.download_link; }
|
||||
set download_link(link: string) { this._update.download_link = link; }
|
||||
|
||||
get can_update(): boolean { return this._update.can_update; }
|
||||
set can_update(can: boolean) {
|
||||
this._update.can_update = can;
|
||||
const download = document.getElementById("update-download") as HTMLSpanElement;
|
||||
const update = document.getElementById("update-update") as HTMLSpanElement;
|
||||
if (can) {
|
||||
download.classList.add("unfocused");
|
||||
update.classList.remove("unfocused");
|
||||
} else {
|
||||
download.onclick = () => window.open(this._update.download_link || this._update.link);
|
||||
download.classList.remove("unfocused");
|
||||
update.classList.add("unfocused");
|
||||
}
|
||||
}
|
||||
|
||||
get update(): Update { return this._update; }
|
||||
set update(update: Update) {
|
||||
this._update = update;
|
||||
this.version = update.version;
|
||||
this.commit = update.commit;
|
||||
this.date = update.date;
|
||||
this.description = update.description;
|
||||
this.changelog = update.changelog;
|
||||
this.link = update.link;
|
||||
this.download_link = update.download_link;
|
||||
this.can_update = update.can_update;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const update = document.getElementById("update-update") as HTMLSpanElement;
|
||||
update.onclick = () => {
|
||||
toggleLoader(update);
|
||||
_post("/config/update", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(update);
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
|
||||
} else {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateApplied"));
|
||||
}
|
||||
window.modals.updateInfo.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
this.checkForUpdates(() => {
|
||||
this.updateAvailable = true;
|
||||
window.notifications.customPositive("updateAvailable", "", window.lang.notif("updateAvailable"));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,487 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://builds.hrfee.pw"
|
||||
namespace = "hrfee"
|
||||
repo = "jfa-go"
|
||||
)
|
||||
|
||||
type GHRelease struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
ID int `json:"id"`
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Assets []GHAsset `json:"assets"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type GHAsset struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type UnixTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (t *UnixTime) UnmarshalJSON(b []byte) (err error) {
|
||||
unix, err := strconv.ParseInt(strings.TrimPrefix(strings.TrimSuffix(string(b), "\""), "\""), 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Time = time.Unix(unix, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func (t UnixTime) MarshalJSON() ([]byte, error) {
|
||||
if t.Time == (time.Time{}) {
|
||||
return []byte("\"\""), nil
|
||||
}
|
||||
return []byte("\"" + strconv.FormatInt(t.Time.Unix(), 10) + "\""), nil
|
||||
}
|
||||
|
||||
var updater string
|
||||
|
||||
type BuildType int
|
||||
|
||||
const (
|
||||
off BuildType = iota
|
||||
internal // Internal assets through go:embed, no data/.
|
||||
external // External assets in data/, accesses through app.localFS.
|
||||
docker // Only notify of new updates, no self-updating.
|
||||
)
|
||||
|
||||
type ApplyUpdate func() error
|
||||
|
||||
type Update struct {
|
||||
Version string `json:"version"` // vX.X.X or git
|
||||
Commit string `json:"commit"`
|
||||
ReleaseDate int64 `json:"date"` // unix time
|
||||
Description string `json:"description"` // Commit Name/Release title.
|
||||
Changelog string `json:"changelog"` // Changelog, if applicable
|
||||
Link string `json:"link"` // Link to commit/release page,
|
||||
DownloadLink string `json:"download_link"` // Optional link to download page.
|
||||
CanUpdate bool `json:"can_update"` // Whether or not update can be done automatically.
|
||||
update ApplyUpdate `json:"-"` // Function to apply update if possible.
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Ready bool `json:"ready"` // Whether or not build on this tag has completed.
|
||||
Version string `json:"version,omitempty"` // Version/Commit
|
||||
ReleaseDate UnixTime `json:"date"`
|
||||
}
|
||||
|
||||
var goos = map[string]string{
|
||||
"darwin": "macOS",
|
||||
"linux": "Linux",
|
||||
"windows": "Windows",
|
||||
}
|
||||
|
||||
var goarch = map[string]string{
|
||||
"amd64": "x86_64",
|
||||
"arm64": "arm64",
|
||||
"arm": "armv6",
|
||||
}
|
||||
|
||||
// func newDockerBuild() Update {
|
||||
// var tag string
|
||||
// if version == "git" {
|
||||
// tag = "docker-unstable"
|
||||
// } else {
|
||||
// tag = "docker-latest"
|
||||
// }
|
||||
// }
|
||||
type Updater struct {
|
||||
version, commit, tag, url, namespace, name string
|
||||
stable bool
|
||||
buildType BuildType
|
||||
httpClient *http.Client
|
||||
timeoutHandler common.TimeoutHandler
|
||||
binary string
|
||||
}
|
||||
|
||||
func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
|
||||
bType := off
|
||||
fmt.Println("BT", buildType)
|
||||
tag := ""
|
||||
switch buildType {
|
||||
case "binary":
|
||||
if binaryType == "internal" {
|
||||
bType = internal
|
||||
tag = "internal"
|
||||
} else {
|
||||
bType = external
|
||||
tag = "external"
|
||||
}
|
||||
case "docker":
|
||||
bType = docker
|
||||
if version == "git" {
|
||||
tag = "docker-unstable"
|
||||
} else {
|
||||
tag = "docker-latest"
|
||||
}
|
||||
default:
|
||||
bType = off
|
||||
}
|
||||
if commit == "unknown" {
|
||||
bType = off
|
||||
}
|
||||
if version == "git" && bType != docker {
|
||||
tag += "-git"
|
||||
}
|
||||
binary := "jfa-go"
|
||||
if runtime.GOOS == "windows" {
|
||||
binary += ".exe"
|
||||
}
|
||||
return &Updater{
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true),
|
||||
version: version,
|
||||
commit: commit,
|
||||
buildType: bType,
|
||||
tag: tag,
|
||||
url: buildroneURL,
|
||||
namespace: namespace,
|
||||
name: repo,
|
||||
binary: binary,
|
||||
}
|
||||
}
|
||||
|
||||
type BuildDTO struct {
|
||||
ID int64 // `json:"id"`
|
||||
Name string // `json:"name"`
|
||||
Date time.Time // `json:"date"`
|
||||
Link string // `json:"link"`
|
||||
Message string
|
||||
Branch string // `json:"branch"`
|
||||
Tags map[string]Tag
|
||||
}
|
||||
|
||||
func (ud *Updater) GetTag() (Tag, int, error) {
|
||||
if ud.buildType == off {
|
||||
return Tag{}, -1, nil
|
||||
}
|
||||
url := fmt.Sprintf("%s/repo/%s/%s/tag/latest/%s", ud.url, ud.namespace, ud.name, ud.tag)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := ud.httpClient.Do(req)
|
||||
defer ud.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return Tag{}, resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Tag{}, -1, err
|
||||
}
|
||||
|
||||
var tag Tag
|
||||
err = json.Unmarshal(body, &tag)
|
||||
return tag, resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (t *Tag) IsNew() bool {
|
||||
return t.Version == version
|
||||
}
|
||||
|
||||
func (ud *Updater) getRelease() (release GHRelease, status int, err error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", ud.namespace, ud.name)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := ud.httpClient.Do(req)
|
||||
status = resp.StatusCode
|
||||
defer ud.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &release)
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *Updater) GetUpdate(tag Tag) (update Update, status int, err error) {
|
||||
switch ud.buildType {
|
||||
case internal:
|
||||
if ud.tag == "internal-git" {
|
||||
update, status, err = ud.getUpdateInternalGit(tag)
|
||||
} else if ud.tag == "internal" {
|
||||
update, status, err = ud.getUpdateInternal(tag)
|
||||
}
|
||||
case external, docker:
|
||||
if strings.Contains(ud.tag, "git") || ud.tag == "docker-unstable" {
|
||||
update, status, err = ud.getCommitGit(tag)
|
||||
} else {
|
||||
var release GHRelease
|
||||
release, status, err = ud.getRelease()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
update = Update{
|
||||
Changelog: release.Body,
|
||||
Description: release.Name,
|
||||
Version: release.TagName,
|
||||
Commit: tag.Version,
|
||||
Link: release.HTMLURL,
|
||||
ReleaseDate: release.PublishedAt.Unix(),
|
||||
}
|
||||
}
|
||||
if ud.buildType == docker {
|
||||
update.DownloadLink = fmt.Sprintf("https://hub.docker.com/r/%s/%s/tags", ud.namespace, ud.name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *Updater) getUpdateInternal(tag Tag) (update Update, status int, err error) {
|
||||
release, status, err := ud.getRelease()
|
||||
update = Update{
|
||||
Changelog: release.Body,
|
||||
Description: release.Name,
|
||||
Version: release.TagName,
|
||||
Commit: tag.Version,
|
||||
Link: release.HTMLURL,
|
||||
ReleaseDate: release.PublishedAt.Unix(),
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return
|
||||
}
|
||||
updateFunc, status, err := ud.downloadInternal(&release.Assets, tag)
|
||||
if err == nil && status == 200 {
|
||||
update.CanUpdate = true
|
||||
update.update = updateFunc
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *Updater) getCommitGit(tag Tag) (update Update, status int, err error) {
|
||||
url := fmt.Sprintf("%s/repo/%s/%s/build/%s", ud.url, ud.namespace, ud.name, tag.Version)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := ud.httpClient.Do(req)
|
||||
status = resp.StatusCode
|
||||
defer ud.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var build BuildDTO
|
||||
err = json.Unmarshal(body, &build)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
update = Update{
|
||||
Description: build.Name,
|
||||
Version: "git",
|
||||
Commit: tag.Version,
|
||||
Link: build.Link,
|
||||
ReleaseDate: tag.ReleaseDate.Unix(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *Updater) getUpdateInternalGit(tag Tag) (update Update, status int, err error) {
|
||||
update, status, err = ud.getCommitGit(tag)
|
||||
if err != nil || status != 200 {
|
||||
return
|
||||
}
|
||||
updateFunc, status, err := ud.downloadInternalGit()
|
||||
if err == nil && status == 200 {
|
||||
update.CanUpdate = true
|
||||
update.update = updateFunc
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getBuildName() string {
|
||||
operatingSystem, ok := goos[runtime.GOOS]
|
||||
if !ok {
|
||||
for _, v := range goos {
|
||||
if strings.Contains(v, runtime.GOOS) {
|
||||
operatingSystem = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if operatingSystem == "" {
|
||||
return ""
|
||||
}
|
||||
arch, ok := goarch[runtime.GOARCH]
|
||||
if !ok {
|
||||
for _, v := range goarch {
|
||||
if strings.Contains(v, runtime.GOARCH) {
|
||||
arch = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
return operatingSystem + "_" + arch
|
||||
}
|
||||
|
||||
func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) {
|
||||
return ud.pullInternal(ud.getInternalURL(assets, tag))
|
||||
}
|
||||
|
||||
func (ud *Updater) downloadInternalGit() (applyUpdate ApplyUpdate, status int, err error) {
|
||||
return ud.pullInternal(ud.getInternalGitURL())
|
||||
}
|
||||
|
||||
func (ud *Updater) getInternalURL(assets *[]GHAsset, tag Tag) string {
|
||||
buildName := getBuildName()
|
||||
if buildName == "" {
|
||||
return ""
|
||||
}
|
||||
url := ""
|
||||
for _, asset := range *assets {
|
||||
if strings.Contains(asset.Name, buildName) {
|
||||
url = asset.BrowserDownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (ud *Updater) getInternalGitURL() string {
|
||||
buildName := getBuildName()
|
||||
if buildName == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/repo/%s/%s/latest/file/%s", ud.url, ud.namespace, ud.name, buildName)
|
||||
}
|
||||
|
||||
func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int, err error) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := ud.httpClient.Do(req)
|
||||
status = resp.StatusCode
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
tarReader := tar.NewReader(gz)
|
||||
var header *tar.Header
|
||||
for {
|
||||
header, err = tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
switch header.Typeflag {
|
||||
case tar.TypeReg:
|
||||
// Search only for file named ud.binary
|
||||
if header.Name == ud.binary {
|
||||
applyUpdate = func() error {
|
||||
defer gz.Close()
|
||||
defer resp.Body.Close()
|
||||
file, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := info.Mode()
|
||||
f, err := os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, tarReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(path+"_", path)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = errors.New("Couldn't find file: " + ud.binary)
|
||||
return
|
||||
}
|
||||
|
||||
// func newInternalBuild() Update {
|
||||
// tag := "internal"
|
||||
|
||||
// func update(path string) err {
|
||||
// if
|
||||
// fp, err := os.Executable()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// fullPath, err := filepath.EvalSymlinks(fp)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// newBinary,
|
||||
// }
|
||||
func (app *appContext) checkForUpdates() {
|
||||
for {
|
||||
go func() {
|
||||
tag, status, err := app.updater.GetTag()
|
||||
if status != 200 || err != nil {
|
||||
if strings.Contains(err.Error(), "strconv.ParseInt") {
|
||||
app.err.Println("No new updates available.")
|
||||
} else {
|
||||
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tag != app.tag && tag.IsNew() {
|
||||
app.info.Println("Update found")
|
||||
update, status, err := app.updater.GetUpdate(tag)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get update (%d): %v", status, err)
|
||||
return
|
||||
}
|
||||
app.tag = tag
|
||||
app.update = update
|
||||
app.newUpdate = true
|
||||
}
|
||||
}()
|
||||
time.Sleep(30 * time.Minute)
|
||||
}
|
||||
}
|
Loading…
Reference in new issue