Skip to content

Commit 83fd02b

Browse files
authored
FAI-8012 - Octopus should create Deploy->Artifact->Commit connections if possible (#1173)
1 parent 2173d38 commit 83fd02b

File tree

4 files changed

+254
-4
lines changed

4 files changed

+254
-4
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Octopus",
3+
"description": "Configuration options that apply to records generated by the Octopus Source.",
4+
"type": "object",
5+
"oneOf": [
6+
{
7+
"type": "object",
8+
"title": "Configuration",
9+
"properties": {
10+
"source_type": {
11+
"type": "string",
12+
"const": "Octopus",
13+
"order": 0
14+
},
15+
"vcs_source": {
16+
"type": "string",
17+
"title": "VCS Source",
18+
"description": "The VCS source for the VCS information captured in Octopus",
19+
"default": "GitHub"
20+
}
21+
}
22+
}
23+
]
24+
}

destinations/airbyte-faros-destination/resources/source-specific-configs/spec.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
"notion": {
5252
"$ref": "notion.json"
5353
},
54+
"octopus": {
55+
"$ref": "octopus.json"
56+
},
5457
"opsgenie": {
5558
"$ref": "opsgenie.json"
5659
},
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {AirbyteRecord} from 'faros-airbyte-cdk';
22

3-
import {Converter} from '../converter';
3+
import {Converter, StreamContext} from '../converter';
4+
5+
interface OctopusConfig {
6+
vcs_source?: string;
7+
}
48

59
/** Octopus converter base */
610
export abstract class OctopusConverter extends Converter {
@@ -9,4 +13,12 @@ export abstract class OctopusConverter extends Converter {
913
id(record: AirbyteRecord): any {
1014
return record?.record?.data?.Id;
1115
}
16+
17+
protected octopusConfig(ctx: StreamContext): OctopusConfig {
18+
return ctx.config.source_specific_configs?.octopus ?? {};
19+
}
20+
21+
protected vcsSource(ctx: StreamContext): string | undefined {
22+
return this.octopusConfig(ctx)?.vcs_source;
23+
}
1224
}

destinations/airbyte-faros-destination/src/converters/octopus/deployments.ts

Lines changed: 214 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
import {AirbyteRecord} from 'faros-airbyte-cdk';
1+
import {AirbyteLogger, AirbyteRecord} from 'faros-airbyte-cdk';
22
import {Utils} from 'faros-js-client';
3+
import GitUrlParse from 'git-url-parse';
34

45
import {Common} from '../common/common';
56
import {DestinationModel, DestinationRecord, StreamContext} from '../converter';
67
import {OctopusConverter} from './common';
78

9+
interface ArtifactVCSInfo {
10+
artifactUid: string;
11+
sha?: string;
12+
repository: {
13+
name: string;
14+
organization: {
15+
uid: string;
16+
source: string;
17+
};
18+
};
19+
}
20+
821
export class Deployments extends OctopusConverter {
922
readonly destinationModels: ReadonlyArray<DestinationModel> = [
1023
'cicd_Deployment',
@@ -18,10 +31,15 @@ export class Deployments extends OctopusConverter {
1831
const source = this.streamName.source;
1932
const deployment = record.record.data;
2033

34+
const deploymentKey = {
35+
uid: deployment.Id,
36+
source,
37+
};
38+
2139
res.push({
2240
model: 'cicd_Deployment',
2341
record: {
24-
uid: deployment.Id,
42+
...deploymentKey,
2543
application: Common.computeApplication(deployment.ProjectName),
2644
url: deployment.Links?.Self,
2745
requestedAt: Utils.toDate(deployment.Task?.QueueTime),
@@ -32,13 +50,206 @@ export class Deployments extends OctopusConverter {
3250
deployment.Task?.State,
3351
deployment.Task?.ErrorMessage
3452
),
35-
source,
3653
},
3754
});
3855

56+
const vcsInfo = this.getVCSInfo(deployment, ctx);
57+
58+
for (const vcs of vcsInfo) {
59+
const artifactKey = {
60+
uid: vcs.artifactUid,
61+
repository: {
62+
uid: vcs.repository.name,
63+
organization: vcs.repository.organization,
64+
},
65+
};
66+
67+
res.push({
68+
model: 'cicd_ArtifactDeployment',
69+
record: {
70+
artifact: artifactKey,
71+
deployment: deploymentKey,
72+
},
73+
});
74+
75+
if (vcs.sha) {
76+
// Only instantiate artifact if we can link all the way to commit
77+
res.push({
78+
model: 'cicd_Artifact',
79+
record: artifactKey,
80+
});
81+
res.push({
82+
model: 'cicd_ArtifactCommitAssociation',
83+
record: {
84+
artifact: artifactKey,
85+
commit: {
86+
sha: vcs.sha,
87+
repository: vcs.repository,
88+
},
89+
},
90+
});
91+
}
92+
}
93+
3994
return res;
4095
}
4196

97+
private getVCSInfo(deployment: any, ctx: StreamContext): ArtifactVCSInfo[] {
98+
const deployId = deployment.Id;
99+
const logger = ctx.logger;
100+
101+
let commitSha: string;
102+
let repoName: string;
103+
let orgUid: string;
104+
let orgSource: string = this.vcsSource(ctx);
105+
106+
// Attempt to first retrieve VCS information from explicit deployment variables
107+
for (const v of deployment.Variables ?? []) {
108+
if (v.Name === 'VCS_COMMIT') {
109+
commitSha = this.getVarValue(deployId, v, logger);
110+
} else if (v.name === 'VCS_REPO') {
111+
repoName = this.getVarValue(deployId, v, logger);
112+
} else if (v.name === 'VCS_ORG') {
113+
orgUid = this.getVarValue(deployId, v, logger);
114+
} else if (v.name === 'VCS_SOURCE') {
115+
orgSource = this.getVarValue(deployId, v, logger);
116+
}
117+
}
118+
119+
// If all information provided as variables return it
120+
if (commitSha && repoName && orgUid) {
121+
return [
122+
{
123+
artifactUid: commitSha,
124+
sha: commitSha,
125+
repository: {
126+
name: repoName,
127+
organization: {
128+
uid: orgUid,
129+
source: orgSource,
130+
},
131+
},
132+
},
133+
];
134+
}
135+
136+
const vcsInfo: ArtifactVCSInfo[] = [];
137+
const hasChangeArray =
138+
deployment.Changes &&
139+
Array.isArray(deployment.Changes) &&
140+
deployment.Changes.length > 0;
141+
142+
if (hasChangeArray) {
143+
// First try to construct the fallback repo from explicit deployment variables
144+
let fallbackRepoInfo: ArtifactVCSInfo['repository'];
145+
if (repoName && orgUid) {
146+
fallbackRepoInfo = {
147+
name: repoName,
148+
organization: {
149+
uid: orgUid,
150+
source: orgSource,
151+
},
152+
};
153+
}
154+
155+
// If fallback not set attempt to retrieve it from repo property in process
156+
if (!fallbackRepoInfo) {
157+
for (const step of deployment.Process?.Steps ?? []) {
158+
for (const action of step.Actions ?? []) {
159+
const repo = action.Properties?.['repo'];
160+
if (repo) {
161+
fallbackRepoInfo = this.getRepoInfoFromURL(deployId, repo, ctx);
162+
break;
163+
}
164+
}
165+
if (fallbackRepoInfo) {
166+
break;
167+
}
168+
}
169+
}
170+
171+
// Assume last change was the one deployed
172+
const change = deployment.Changes.at(-1);
173+
174+
// Retrieve repo information from Commits
175+
for (const commit of change.Commits ?? []) {
176+
const artifactUid = change.Version;
177+
const sha = commit.Id;
178+
const repository =
179+
this.getRepoInfoFromURL(deployId, commit.LinkUrl, ctx) ??
180+
fallbackRepoInfo;
181+
182+
if (repository) {
183+
vcsInfo.push({artifactUid, sha, repository});
184+
}
185+
}
186+
187+
// Commits did not yield any VCS info, use BuildInformation
188+
if (!vcsInfo.length) {
189+
for (const buildInfo of change.BuildInformation ?? []) {
190+
const artifactUid = `${buildInfo.PackageId}:${buildInfo.Version}`;
191+
const sha = buildInfo.VcsCommitNumber;
192+
const repository =
193+
this.getRepoInfoFromURL(deployId, buildInfo.VcsRoot, ctx) ??
194+
fallbackRepoInfo;
195+
196+
if (repository) {
197+
vcsInfo.push({artifactUid, sha, repository});
198+
}
199+
}
200+
}
201+
202+
// If still no info create a dummy artifact using Change.Version
203+
if (!vcsInfo.length && fallbackRepoInfo) {
204+
vcsInfo.push({
205+
artifactUid: change.Version,
206+
repository: fallbackRepoInfo,
207+
});
208+
}
209+
}
210+
211+
if (!vcsInfo.length) {
212+
logger.warn(`Could not retrieve VCS info for deployment: ${deployId}`);
213+
}
214+
215+
return vcsInfo;
216+
}
217+
218+
private getRepoInfoFromURL(
219+
deploymentId: string,
220+
url: string,
221+
ctx: StreamContext
222+
): ArtifactVCSInfo['repository'] {
223+
try {
224+
const parsedUrl = GitUrlParse(url);
225+
return {
226+
name: parsedUrl.name.toLowerCase(),
227+
organization: {
228+
uid: parsedUrl.owner.toLowerCase(),
229+
source: this.vcsSource(ctx),
230+
},
231+
};
232+
} catch (err: any) {
233+
ctx.logger.warn(
234+
`Unable to parse VCS information for deployment ${deploymentId} from repo url: ${url}`
235+
);
236+
}
237+
}
238+
239+
private getVarValue(
240+
deploymentId: string,
241+
variable: {Name: string; Value: string},
242+
logger: AirbyteLogger
243+
): string | undefined {
244+
if (variable.Value) {
245+
return variable.Value;
246+
}
247+
logger.warn(
248+
`Deployment [${deploymentId}] had a null value for ${variable.Name} variable`
249+
);
250+
return undefined;
251+
}
252+
42253
/**
43254
* Octopus task statuses include:
44255
* Canceled, Cancelling, Executing, Failed, Queued, Success, TimedOut

0 commit comments

Comments
 (0)