mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
fix(ado): Manually pass token through http header for ado server (#543)
* support passing in token manually in auth header * remove unneeded PAT embed check * cleanup authheader usage * changelog * var name typo * unset auth header in fetch * move unset to finally in fetch
This commit is contained in:
parent
7a97d4ee06
commit
aa62847143
17 changed files with 89 additions and 40 deletions
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Manually pass auth token for ado server deployments. [#543](https://github.com/sourcebot-dev/sourcebot/pull/543)
|
||||||
|
|
||||||
## [4.7.2] - 2025-09-22
|
## [4.7.2] - 2025-09-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
"repos": [
|
"repos": [
|
||||||
"organizationName/projectName/repoName",
|
"organizationName/projectName/repoName",
|
||||||
"organizationName/projectName/repoName2
|
"organizationName/projectName/repoName2
|
||||||
|
|
@ -26,6 +27,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
"orgs": [
|
"orgs": [
|
||||||
"organizationName",
|
"organizationName",
|
||||||
"organizationName2
|
"organizationName2
|
||||||
|
|
@ -37,6 +39,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
"projects": [
|
"projects": [
|
||||||
"organizationName/projectName",
|
"organizationName/projectName",
|
||||||
"organizationName/projectName2"
|
"organizationName/projectName2"
|
||||||
|
|
@ -48,6 +51,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
// Include all repos in my-org...
|
// Include all repos in my-org...
|
||||||
"orgs": [
|
"orgs": [
|
||||||
"my-org"
|
"my-org"
|
||||||
|
|
@ -91,6 +95,7 @@ Next, provide the access token via the `token` property, either as an environmen
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
"token": {
|
"token": {
|
||||||
// note: this env var can be named anything. It
|
// note: this env var can be named anything. It
|
||||||
// doesn't need to be `ADO_TOKEN`.
|
// doesn't need to be `ADO_TOKEN`.
|
||||||
|
|
@ -121,6 +126,7 @@ Next, provide the access token via the `token` property, either as an environmen
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "cloud",
|
||||||
"token": {
|
"token": {
|
||||||
"secret": "mysecret"
|
"secret": "mysecret"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
"useTfsPath": true
|
"deploymentType": "server",
|
||||||
|
"useTfsPath": true,
|
||||||
"repos": [
|
"repos": [
|
||||||
"organizationName/projectName/repoName",
|
"organizationName/projectName/repoName",
|
||||||
"organizationName/projectName/repoName2
|
"organizationName/projectName/repoName2
|
||||||
|
|
@ -28,6 +29,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
"repos": [
|
"repos": [
|
||||||
"organizationName/projectName/repoName",
|
"organizationName/projectName/repoName",
|
||||||
"organizationName/projectName/repoName2
|
"organizationName/projectName/repoName2
|
||||||
|
|
@ -39,6 +41,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
"orgs": [
|
"orgs": [
|
||||||
"collectionName",
|
"collectionName",
|
||||||
"collectionName2"
|
"collectionName2"
|
||||||
|
|
@ -50,6 +53,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
"projects": [
|
"projects": [
|
||||||
"collectionName/projectName",
|
"collectionName/projectName",
|
||||||
"collectionName/projectName2"
|
"collectionName/projectName2"
|
||||||
|
|
@ -61,6 +65,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
// Include all repos in my-org...
|
// Include all repos in my-org...
|
||||||
"orgs": [
|
"orgs": [
|
||||||
"my-org"
|
"my-org"
|
||||||
|
|
@ -104,6 +109,7 @@ Next, provide the access token via the `token` property, either as an environmen
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
"token": {
|
"token": {
|
||||||
// note: this env var can be named anything. It
|
// note: this env var can be named anything. It
|
||||||
// doesn't need to be `ADO_TOKEN`.
|
// doesn't need to be `ADO_TOKEN`.
|
||||||
|
|
@ -134,6 +140,7 @@ Next, provide the access token via the `token` property, either as an environmen
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "azuredevops",
|
"type": "azuredevops",
|
||||||
|
"deploymentType": "server",
|
||||||
"token": {
|
"token": {
|
||||||
"secret": "mysecret"
|
"secret": "mysecret"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -199,7 +198,8 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -931,7 +931,6 @@
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -1068,7 +1067,8 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1214,7 +1214,6 @@
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -1351,7 +1350,8 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ type onProgressFn = (event: SimpleGitProgressEvent) => void;
|
||||||
export const cloneRepository = async (
|
export const cloneRepository = async (
|
||||||
{
|
{
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
|
authHeader,
|
||||||
path,
|
path,
|
||||||
onProgress,
|
onProgress,
|
||||||
}: {
|
}: {
|
||||||
cloneUrl: string,
|
cloneUrl: string,
|
||||||
|
authHeader?: string,
|
||||||
path: string,
|
path: string,
|
||||||
onProgress?: onProgressFn
|
onProgress?: onProgressFn
|
||||||
}
|
}
|
||||||
|
|
@ -24,13 +26,12 @@ export const cloneRepository = async (
|
||||||
path,
|
path,
|
||||||
})
|
})
|
||||||
|
|
||||||
await git.clone(
|
const cloneArgs = [
|
||||||
cloneUrl,
|
|
||||||
path,
|
|
||||||
[
|
|
||||||
"--bare",
|
"--bare",
|
||||||
]
|
...(authHeader ? ["-c", `http.extraHeader=${authHeader}`] : [])
|
||||||
);
|
];
|
||||||
|
|
||||||
|
await git.clone(cloneUrl, path, cloneArgs);
|
||||||
|
|
||||||
await unsetGitConfig(path, ["remote.origin.url"]);
|
await unsetGitConfig(path, ["remote.origin.url"]);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|
@ -50,10 +51,12 @@ export const cloneRepository = async (
|
||||||
export const fetchRepository = async (
|
export const fetchRepository = async (
|
||||||
{
|
{
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
|
authHeader,
|
||||||
path,
|
path,
|
||||||
onProgress,
|
onProgress,
|
||||||
}: {
|
}: {
|
||||||
cloneUrl: string,
|
cloneUrl: string,
|
||||||
|
authHeader?: string,
|
||||||
path: string,
|
path: string,
|
||||||
onProgress?: onProgressFn
|
onProgress?: onProgressFn
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +68,10 @@ export const fetchRepository = async (
|
||||||
path: path,
|
path: path,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (authHeader) {
|
||||||
|
await git.addConfig("http.extraHeader", authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
await git.fetch([
|
await git.fetch([
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
"+refs/heads/*:refs/heads/*",
|
"+refs/heads/*:refs/heads/*",
|
||||||
|
|
@ -81,6 +88,16 @@ export const fetchRepository = async (
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`${baseLog}. Error: ${error}`);
|
throw new Error(`${baseLog}. Error: ${error}`);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (authHeader) {
|
||||||
|
const git = simpleGit({
|
||||||
|
progress: onProgress,
|
||||||
|
}).cwd({
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
|
||||||
|
await git.raw(["config", "--unset", "http.extraHeader", authHeader]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ export class RepoManager {
|
||||||
|
|
||||||
const credentials = await getAuthCredentialsForRepo(repo, this.db);
|
const credentials = await getAuthCredentialsForRepo(repo, this.db);
|
||||||
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
|
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
|
||||||
|
const authHeader = credentials?.authHeader ?? undefined;
|
||||||
|
|
||||||
if (existsSync(repoPath) && !isReadOnly) {
|
if (existsSync(repoPath) && !isReadOnly) {
|
||||||
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
|
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
|
||||||
|
|
@ -188,6 +189,7 @@ export class RepoManager {
|
||||||
logger.info(`Fetching ${repo.displayName}...`);
|
logger.info(`Fetching ${repo.displayName}...`);
|
||||||
const { durationMs } = await measure(() => fetchRepository({
|
const { durationMs } = await measure(() => fetchRepository({
|
||||||
cloneUrl: cloneUrlMaybeWithToken,
|
cloneUrl: cloneUrlMaybeWithToken,
|
||||||
|
authHeader,
|
||||||
path: repoPath,
|
path: repoPath,
|
||||||
onProgress: ({ method, stage, progress }) => {
|
onProgress: ({ method, stage, progress }) => {
|
||||||
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
|
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
|
||||||
|
|
@ -203,6 +205,7 @@ export class RepoManager {
|
||||||
|
|
||||||
const { durationMs } = await measure(() => cloneRepository({
|
const { durationMs } = await measure(() => cloneRepository({
|
||||||
cloneUrl: cloneUrlMaybeWithToken,
|
cloneUrl: cloneUrlMaybeWithToken,
|
||||||
|
authHeader,
|
||||||
path: repoPath,
|
path: repoPath,
|
||||||
onProgress: ({ method, stage, progress }) => {
|
onProgress: ({ method, stage, progress }) => {
|
||||||
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
|
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co
|
||||||
export type RepoAuthCredentials = {
|
export type RepoAuthCredentials = {
|
||||||
hostUrl?: string;
|
hostUrl?: string;
|
||||||
token: string;
|
token: string;
|
||||||
cloneUrlWithToken: string;
|
cloneUrlWithToken?: string;
|
||||||
|
authHeader?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +193,17 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
|
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
|
||||||
if (config.token) {
|
if (config.token) {
|
||||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||||
|
|
||||||
|
// For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default
|
||||||
|
// to this over basic auth. As a result, we cannot embed the token in the clone URL and must force basic auth by passing in the token
|
||||||
|
// appropriately in the header. To do this, we set the authHeader field here
|
||||||
|
if (config.deploymentType === 'server') {
|
||||||
|
return {
|
||||||
|
hostUrl: config.url,
|
||||||
|
token,
|
||||||
|
authHeader: "Authorization: Basic " + Buffer.from(`:${token}`).toString('base64')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return {
|
return {
|
||||||
hostUrl: config.url,
|
hostUrl: config.url,
|
||||||
token,
|
token,
|
||||||
|
|
@ -210,6 +221,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ const schema = {
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -198,7 +197,8 @@ const schema = {
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export interface AzureDevOpsConnectionConfig {
|
||||||
/**
|
/**
|
||||||
* The type of Azure DevOps deployment
|
* The type of Azure DevOps deployment
|
||||||
*/
|
*/
|
||||||
deploymentType?: "cloud" | "server";
|
deploymentType: "cloud" | "server";
|
||||||
/**
|
/**
|
||||||
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -930,7 +930,6 @@ const schema = {
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -1067,7 +1066,8 @@ const schema = {
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ export interface AzureDevOpsConnectionConfig {
|
||||||
/**
|
/**
|
||||||
* The type of Azure DevOps deployment
|
* The type of Azure DevOps deployment
|
||||||
*/
|
*/
|
||||||
deploymentType?: "cloud" | "server";
|
deploymentType: "cloud" | "server";
|
||||||
/**
|
/**
|
||||||
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1213,7 +1213,6 @@ const schema = {
|
||||||
"cloud",
|
"cloud",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -1350,7 +1349,8 @@ const schema = {
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -473,7 +473,7 @@ export interface AzureDevOpsConnectionConfig {
|
||||||
/**
|
/**
|
||||||
* The type of Azure DevOps deployment
|
* The type of Azure DevOps deployment
|
||||||
*/
|
*/
|
||||||
deploymentType?: "cloud" | "server";
|
deploymentType: "cloud" | "server";
|
||||||
/**
|
/**
|
||||||
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@
|
||||||
"deploymentType": {
|
"deploymentType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["cloud", "server"],
|
"enum": ["cloud", "server"],
|
||||||
"default": "cloud",
|
|
||||||
"description": "The type of Azure DevOps deployment"
|
"description": "The type of Azure DevOps deployment"
|
||||||
},
|
},
|
||||||
"useTfsPath": {
|
"useTfsPath": {
|
||||||
|
|
@ -129,7 +128,8 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"token"
|
"token",
|
||||||
|
"deploymentType"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue