feat: expose ota version badges
This commit is contained in:
parent
32c6ec3217
commit
ca3fa9ffc3
10
README.md
10
README.md
|
|
@ -17,10 +17,12 @@
|
|||
<a href="https://x.com/intent/follow?screen_name=folo_is"><img src="https://img.shields.io/badge/Follow-blue?color=1d9bf0&logo=x&labelColor=black&style=flat-square" /></a>
|
||||
<a href="https://discord.gg/AwWcAQ7euc" target="_blank"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Ffollowapp%3Fwith_counts%3Dtrue&query=approximate_member_count&color=5865F2&label=Discord&labelColor=black&logo=discord&logoColor=white&style=flat-square"/></a>
|
||||
<br />
|
||||
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/itunes/v/6739802604?style=flat-square&logo=apple&label=App%20Store&color=FF5C00&labelColor=black" /></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=is.follow" target="_blank"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dis.follow%26gl%3DUS%26hl%3Den%26l%3DAndroid%26m%3D%24version&style=flat-square&logo=google-play&label=Google%20Play&labelColor=black&color=FF5C00"/></a>
|
||||
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.folo.is%2Fupdates%2Fdistribution%2Fmas&query=data.storeVersion&prefix=v&style=flat-square&logo=apple&label=Mac%20App%20Store&labelColor=black&color=FF5C00&cacheSeconds=3600" /></a>
|
||||
<a href="https://apps.microsoft.com/detail/9nvfzpv0v0ht?mode=direct"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.folo.is%2Fupdates%2Fdistribution%2Fmss&query=data.storeVersion&style=flat-square&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMyAzaDguNTN2OC41M0gzek0xMi40NjkgM2g4LjUzdjguNTNoLTguNTN6TTMgMTIuNDdoOC41M1YyMUgzek0xMi40NjkgMTIuNDdoOC41M1YyMWgtOC41M3oiLz48L3N2Zz4%3D&logoColor=white&label=Microsoft%20Store&labelColor=black&color=FF5C00&cacheSeconds=3600&prefix=v" /></a>
|
||||
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.store.mobile.ios.version&prefix=v&style=flat-square&logo=apple&label=App%20Store&color=FF5C00&labelColor=black&cacheSeconds=600" /></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=is.follow" target="_blank"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.store.mobile.android.version&prefix=v&style=flat-square&logo=google-play&label=Google%20Play&labelColor=black&color=FF5C00&cacheSeconds=600"/></a>
|
||||
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.store.desktop.mas.version&prefix=v&style=flat-square&logo=apple&label=Mac%20App%20Store&labelColor=black&color=FF5C00&cacheSeconds=600" /></a>
|
||||
<a href="https://apps.microsoft.com/detail/9nvfzpv0v0ht?mode=direct"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.store.desktop.mss.version&prefix=v&style=flat-square&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMyAzaDguNTN2OC41M0gzek0xMi40NjkgM2g4LjUzdjguNTNoLTguNTN6TTMgMTIuNDdoOC41M1YyMUgzek0xMi40NjkgMTIuNDdoOC41M1YyMWgtOC41M3oiLz48L3N2Zz4%3D&logoColor=white&label=Microsoft%20Store&labelColor=black&color=FF5C00&cacheSeconds=600" /></a>
|
||||
<a href="https://github.com/RSSNext/Folo/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.github.mobile.version&prefix=v&style=flat-square&logo=github&label=GitHub%20Mobile&labelColor=black&color=FF5C00&cacheSeconds=600" /></a>
|
||||
<a href="https://github.com/RSSNext/Folo/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fota.folo.is%2Fversions&query=%24.github.desktop.version&prefix=v&style=flat-square&logo=github&label=GitHub%20Desktop&labelColor=black&color=FF5C00&cacheSeconds=600" /></a>
|
||||
<br />
|
||||
<br />
|
||||
<!-- <a href="https://github.com/RSSNext/Folo" target="_blank"><img src="https://github.com/user-attachments/assets/59b957fb-59ed-4ef0-994e-f6a402a6fe2b" alt="GitHub Trending" height="55"/></a>
|
||||
|
|
|
|||
|
|
@ -1541,7 +1541,7 @@ describe("syncStoreVersions", () => {
|
|||
}
|
||||
|
||||
if (url === "https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx") {
|
||||
const body = String(init?.body ?? "")
|
||||
const body = String(_init?.body ?? "")
|
||||
|
||||
if (body.includes("GetCookie")) {
|
||||
return new Response("<EncryptedData>cookie-value</EncryptedData>", {
|
||||
|
|
@ -1782,6 +1782,207 @@ describe("internal routes", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("/versions", () => {
|
||||
it("returns cached store versions and latest product release versions", async () => {
|
||||
const response = await fetchWorker("/versions", undefined, {
|
||||
kvEntries: new Map([
|
||||
[
|
||||
KV_KEYS.storeVersion("mobile", "ios"),
|
||||
{
|
||||
version: "0.4.1",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "app-store",
|
||||
},
|
||||
],
|
||||
[
|
||||
KV_KEYS.storeVersion("mobile", "android"),
|
||||
{
|
||||
version: "0.4.1",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "google-play",
|
||||
},
|
||||
],
|
||||
[
|
||||
KV_KEYS.storeVersion("desktop", "mas"),
|
||||
{
|
||||
version: "1.5.0",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "mac-app-store",
|
||||
},
|
||||
],
|
||||
[
|
||||
KV_KEYS.storeVersion("desktop", "mss"),
|
||||
{
|
||||
version: "1.5.0",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "microsoft-store",
|
||||
},
|
||||
],
|
||||
[
|
||||
KV_KEYS.latestReleaseVersion("mobile"),
|
||||
{
|
||||
product: "mobile",
|
||||
version: "0.4.1",
|
||||
publishedAt: "2026-04-10T12:00:00Z",
|
||||
tag: "mobile/v0.4.1",
|
||||
},
|
||||
],
|
||||
[
|
||||
KV_KEYS.latestReleaseVersion("desktop"),
|
||||
{
|
||||
product: "desktop",
|
||||
version: "1.5.0",
|
||||
publishedAt: "2026-04-11T10:00:00Z",
|
||||
tag: "desktop/v1.5.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
envOverrides: {
|
||||
GITHUB_OWNER: "RSSNext",
|
||||
GITHUB_REPO: "Folo",
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
store: {
|
||||
mobile: {
|
||||
ios: {
|
||||
version: "0.4.1",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "app-store",
|
||||
url: "https://apps.apple.com/us/app/folo-follow-everything/id6739802604",
|
||||
},
|
||||
android: {
|
||||
version: "0.4.1",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "google-play",
|
||||
url: "https://play.google.com/store/apps/details?id=is.follow",
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
mas: {
|
||||
version: "1.5.0",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "mac-app-store",
|
||||
url: "https://apps.apple.com/us/app/folo-follow-everything/id6739802604?platform=mac",
|
||||
},
|
||||
mss: {
|
||||
version: "1.5.0",
|
||||
fetchedAt: "2026-04-12T14:25:27.016Z",
|
||||
source: "microsoft-store",
|
||||
url: "https://apps.microsoft.com/detail/9nvfzpv0v0ht?mode=direct",
|
||||
},
|
||||
},
|
||||
},
|
||||
github: {
|
||||
mobile: {
|
||||
version: "0.4.1",
|
||||
publishedAt: "2026-04-10T12:00:00Z",
|
||||
tag: "mobile/v0.4.1",
|
||||
url: "https://github.com/RSSNext/Folo/releases/tag/mobile/v0.4.1",
|
||||
},
|
||||
desktop: {
|
||||
version: "1.5.0",
|
||||
publishedAt: "2026-04-11T10:00:00Z",
|
||||
tag: "desktop/v1.5.0",
|
||||
url: "https://github.com/RSSNext/Folo/releases/tag/desktop/v1.5.0",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("latest release summary", () => {
|
||||
it("persists the latest release version for each product during github sync", async () => {
|
||||
const kvEntries = new Map<string, unknown>()
|
||||
const mobileRelease = await createReleaseMetadata({
|
||||
releaseVersion: "0.4.3",
|
||||
releaseKind: "store",
|
||||
runtimeVersion: "0.4.3",
|
||||
publishedAt: "2026-04-10T16:00:00Z",
|
||||
git: {
|
||||
tag: "mobile/v0.4.3",
|
||||
commit: "abcdef1234567895",
|
||||
},
|
||||
platforms: {},
|
||||
})
|
||||
const desktopRelease = createDesktopReleaseMetadata({
|
||||
releaseVersion: "1.5.2",
|
||||
releaseKind: "binary",
|
||||
runtimeVersion: null,
|
||||
publishedAt: "2026-04-11T12:00:00Z",
|
||||
git: {
|
||||
tag: "desktop/v1.5.2",
|
||||
commit: "abcdef1234567891",
|
||||
},
|
||||
desktop: {
|
||||
renderer: null,
|
||||
app: null,
|
||||
},
|
||||
})
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: string | URL | Request) => {
|
||||
const url = String(input)
|
||||
|
||||
if (url === "https://api.github.com/repos/RSSNext/Folo/releases") {
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
createGitHubReleaseAssetSet(
|
||||
"desktop/v1.5.2",
|
||||
"https://example.com/desktop.json",
|
||||
null,
|
||||
),
|
||||
createGitHubReleaseAssetSet("mobile/v0.4.3", "https://example.com/mobile.json", null),
|
||||
]),
|
||||
{ status: 200 },
|
||||
)
|
||||
}
|
||||
|
||||
if (url === "https://example.com/mobile.json") {
|
||||
return new Response(JSON.stringify(mobileRelease), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
if (url === "https://example.com/desktop.json") {
|
||||
return new Response(JSON.stringify(desktopRelease), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled fetch URL: ${url}`)
|
||||
}),
|
||||
)
|
||||
|
||||
await syncGitHubReleases(
|
||||
createEnv({
|
||||
kvEntries,
|
||||
envOverrides: {
|
||||
GITHUB_OWNER: "RSSNext",
|
||||
GITHUB_REPO: "Folo",
|
||||
GITHUB_TOKEN: "token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(JSON.parse(String(kvEntries.get(KV_KEYS.latestReleaseVersion("mobile"))))).toEqual({
|
||||
product: "mobile",
|
||||
version: "0.4.3",
|
||||
publishedAt: "2026-04-10T16:00:00Z",
|
||||
tag: "mobile/v0.4.3",
|
||||
})
|
||||
expect(JSON.parse(String(kvEntries.get(KV_KEYS.latestReleaseVersion("desktop"))))).toEqual({
|
||||
product: "desktop",
|
||||
version: "1.5.2",
|
||||
publishedAt: "2026-04-11T12:00:00Z",
|
||||
tag: "desktop/v1.5.2",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("scheduled handler", () => {
|
||||
it("dispatches sync work through waitUntil", async () => {
|
||||
const kvEntries = new Map<string, unknown>([[KV_KEYS.githubEtag, '"etag-current"']])
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { assetsRoute } from "./routes/assets"
|
|||
import { internalRoute } from "./routes/internal"
|
||||
import { manifestRoute } from "./routes/manifest"
|
||||
import { policyRoute } from "./routes/policy"
|
||||
import { versionsRoute } from "./routes/versions"
|
||||
|
||||
export const app = new Hono<{ Bindings: Env }>()
|
||||
|
||||
app.route("/", manifestRoute)
|
||||
app.route("/", assetsRoute)
|
||||
app.route("/", policyRoute)
|
||||
app.route("/", versionsRoute)
|
||||
app.route("/", internalRoute)
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const KV_KEYS = {
|
|||
distribution ? `policy:${product}:${channel}:${distribution}` : `policy:${product}:${channel}`,
|
||||
storeVersion: (product: OtaRelease["product"], target: "ios" | "android" | DesktopDistribution) =>
|
||||
`store-version:${product}:${target}`,
|
||||
latestReleaseVersion: (product: OtaRelease["product"]) => `latest-release:${product}`,
|
||||
githubEtag: "github:etag:releases",
|
||||
syncLastSuccessAt: "sync:last-success-at",
|
||||
storeVersionSyncLastSuccessAt: "sync:store-version-last-success-at",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ export interface StoreVersionRecord {
|
|||
source: "app-store" | "google-play" | "mac-app-store" | "microsoft-store"
|
||||
}
|
||||
|
||||
export interface LatestReleaseVersionRecord {
|
||||
product: OtaRelease["product"]
|
||||
version: OtaRelease["releaseVersion"]
|
||||
publishedAt: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
export async function getLatestReleasePointer(
|
||||
kv: KVNamespace,
|
||||
input: {
|
||||
|
|
@ -97,3 +104,17 @@ export async function putStoreVersionRecord(
|
|||
) {
|
||||
await kv.put(KV_KEYS.storeVersion(input.product, input.target), JSON.stringify(input.value))
|
||||
}
|
||||
|
||||
export async function getLatestReleaseVersionRecord(
|
||||
kv: KVNamespace,
|
||||
product: OtaRelease["product"],
|
||||
) {
|
||||
return kv.get<LatestReleaseVersionRecord>(KV_KEYS.latestReleaseVersion(product), "json")
|
||||
}
|
||||
|
||||
export async function putLatestReleaseVersionRecord(
|
||||
kv: KVNamespace,
|
||||
value: LatestReleaseVersionRecord,
|
||||
) {
|
||||
await kv.put(KV_KEYS.latestReleaseVersion(value.product), JSON.stringify(value))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { buildMirroredAssetKey, extractMirroredFiles } from "./archive"
|
|||
import { KV_KEYS } from "./constants"
|
||||
import { listPublishedOtaReleases } from "./github"
|
||||
import type { BinaryPolicyRecord, LatestReleasePointerRecord } from "./kv"
|
||||
import { putBinaryPolicyRecord, putReleaseRecord, putStoreVersionRecord } from "./kv"
|
||||
import {
|
||||
putBinaryPolicyRecord,
|
||||
putLatestReleaseVersionRecord,
|
||||
putReleaseRecord,
|
||||
putStoreVersionRecord,
|
||||
} from "./kv"
|
||||
import { putMirroredFiles } from "./r2"
|
||||
import type { DesktopDistribution, OtaPlatform, OtaProjectedPlatforms, OtaRelease } from "./schema"
|
||||
import { otaReleaseSchema } from "./schema"
|
||||
|
|
@ -49,6 +54,10 @@ export async function syncStoreVersions(env: Env) {
|
|||
}
|
||||
|
||||
async function runSyncGitHubReleases(env: Env) {
|
||||
const latestReleaseByProduct = new Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>()
|
||||
const storedEtag = await env.OTA_KV.get<string>(KV_KEYS.githubEtag)
|
||||
const releasesResult = await listPublishedOtaReleases({
|
||||
owner: env.GITHUB_OWNER,
|
||||
|
|
@ -64,6 +73,7 @@ async function runSyncGitHubReleases(env: Env) {
|
|||
|
||||
for (const releaseSummary of releasesResult.releases) {
|
||||
const release = await fetchReleaseMetadata(releaseSummary.metadataUrl, env)
|
||||
updateLatestReleaseByProduct(latestReleaseByProduct, release)
|
||||
|
||||
if (release.releaseKind === "ota") {
|
||||
if (!releaseSummary.archiveUrl) {
|
||||
|
|
@ -96,6 +106,15 @@ async function runSyncGitHubReleases(env: Env) {
|
|||
await putLatestPolicyRecord(env.OTA_KV, release)
|
||||
}
|
||||
|
||||
for (const [product, latestRelease] of latestReleaseByProduct) {
|
||||
await putLatestReleaseVersionRecord(env.OTA_KV, {
|
||||
product,
|
||||
version: latestRelease.releaseVersion,
|
||||
publishedAt: latestRelease.publishedAt,
|
||||
tag: latestRelease.tag,
|
||||
})
|
||||
}
|
||||
|
||||
if (releasesResult.etag) {
|
||||
await env.OTA_KV.put(KV_KEYS.githubEtag, releasesResult.etag)
|
||||
}
|
||||
|
|
@ -162,6 +181,23 @@ async function runSyncStoreVersions(env: Env) {
|
|||
await env.OTA_KV.put(KV_KEYS.storeVersionSyncLastSuccessAt, fetchedAt)
|
||||
}
|
||||
|
||||
function updateLatestReleaseByProduct(
|
||||
latestReleaseByProduct: Map<
|
||||
OtaRelease["product"],
|
||||
Pick<OtaRelease, "releaseVersion" | "publishedAt"> & { tag: string }
|
||||
>,
|
||||
release: OtaRelease,
|
||||
) {
|
||||
const current = latestReleaseByProduct.get(release.product)
|
||||
if (!current || compareSemver(release.releaseVersion, current.releaseVersion) > 0) {
|
||||
latestReleaseByProduct.set(release.product, {
|
||||
releaseVersion: release.releaseVersion,
|
||||
publishedAt: release.publishedAt,
|
||||
tag: release.git.tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function mirrorReleaseToStorage(
|
||||
input: {
|
||||
release: OtaRelease
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { Hono } from "hono"
|
||||
|
||||
import type { Env } from "../env"
|
||||
import { getLatestReleaseVersionRecord, getStoreVersionRecord } from "../lib/kv"
|
||||
import { STORE_URLS } from "../lib/store-version"
|
||||
|
||||
export const versionsRoute = new Hono<{ Bindings: Env }>()
|
||||
|
||||
versionsRoute.get("/versions", async (c) => {
|
||||
const [ios, android, mas, mss, mobileRelease, desktopRelease] = await Promise.all([
|
||||
getStoreVersionRecord(c.env.OTA_KV, { product: "mobile", target: "ios" }),
|
||||
getStoreVersionRecord(c.env.OTA_KV, { product: "mobile", target: "android" }),
|
||||
getStoreVersionRecord(c.env.OTA_KV, { product: "desktop", target: "mas" }),
|
||||
getStoreVersionRecord(c.env.OTA_KV, { product: "desktop", target: "mss" }),
|
||||
getLatestReleaseVersionRecord(c.env.OTA_KV, "mobile"),
|
||||
getLatestReleaseVersionRecord(c.env.OTA_KV, "desktop"),
|
||||
])
|
||||
|
||||
return c.json({
|
||||
store: {
|
||||
mobile: {
|
||||
ios: toStoreVersionPayload(ios, STORE_URLS.ios),
|
||||
android: toStoreVersionPayload(android, STORE_URLS.android),
|
||||
},
|
||||
desktop: {
|
||||
mas: toStoreVersionPayload(mas, STORE_URLS.mas),
|
||||
mss: toStoreVersionPayload(mss, STORE_URLS.mss),
|
||||
},
|
||||
},
|
||||
github: {
|
||||
mobile: toLatestReleasePayload(c.env, mobileRelease),
|
||||
desktop: toLatestReleasePayload(c.env, desktopRelease),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
function toStoreVersionPayload(
|
||||
record: Awaited<ReturnType<typeof getStoreVersionRecord>>,
|
||||
url: string,
|
||||
) {
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
version: record.version,
|
||||
fetchedAt: record.fetchedAt,
|
||||
source: record.source,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
function toLatestReleasePayload(
|
||||
env: Env,
|
||||
record: Awaited<ReturnType<typeof getLatestReleaseVersionRecord>>,
|
||||
) {
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
version: record.version,
|
||||
publishedAt: record.publishedAt,
|
||||
tag: record.tag,
|
||||
url: `https://github.com/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/releases/tag/${record.tag}`,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue