Skip to content

Commit

Permalink
feat(v3): simplify change SHA management; use median lead time for ch…
Browse files Browse the repository at this point in the history
…ange instead of average value
  • Loading branch information
mikaelvesavuori committed Jun 11, 2023
1 parent 9638de2 commit d4eca99
Show file tree
Hide file tree
Showing 22 changed files with 140 additions and 229 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Files
.env
.dccache
.DS_Store

# Folders
node_modules/
Expand All @@ -16,4 +17,4 @@ jest-coverage/
# Diagrams
diagrams/*.bkp
diagrams/*.bak
diagrams/*.dtmp
diagrams/*.dtmp
27 changes: 8 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ As seen above, the required inputs are:

1. The endpoint
2. The API key
3. The product name
3. The repo name (proxy for the "product")

#### GitHub Actions action

Expand All @@ -213,7 +213,7 @@ steps:
fetch-depth: 0

- name: Dorametrix
uses: mikaelvesavuori/dorametrix-action@v1.0.0
uses: mikaelvesavuori/dorametrix-action@v3
with:
endpoint: ${{ secrets.DORAMETRIX_ENDPOINT }}
api-key: ${{ secrets.DORAMETRIX_API_KEY }}
Expand All @@ -225,6 +225,8 @@ The specific action, `mikaelvesavuori/[email protected]`, is available fo

#### Bitbucket Pipelines pipe

_Note that the version below assumes Dorametrix version 1/2._

An example using two user-provided secrets and setting the product with a known variable representing the repo name:

```yaml
Expand Down Expand Up @@ -389,7 +391,7 @@ All of the below demonstrates "directly calling" the API; since webhook events f
```json
{
"eventType": "change",
"product": "demo"
"repo": "demo"
}
```

Expand All @@ -406,21 +408,8 @@ All of the below demonstrates "directly calling" the API; since webhook events f
```json
{
"eventType": "deployment",
"product": "demo",
"changes": [
{
"id": "356a192b7913b04c54574d18c28d46e6395428ab",
"timeCreated": "1642879177"
},
{
"id": "da4b9237bacccdf19c0760cab7aec4a8359010b0",
"timeCreated": "1642874964"
},
{
"id": "77de68daecd823babbb58edb1c8e14d7106e83bb",
"timeCreated": "1642873353"
}
]
"repo": "demo",
"changeSha": "356a192b7913b04c54574d18c28d46e6395428ab"
}
```

Expand All @@ -437,7 +426,7 @@ All of the below demonstrates "directly calling" the API; since webhook events f
```json
{
"eventType": "incident",
"product": "demo"
"repo": "demo"
}
```

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ These versions of `dorametrix` are currently being supported with security updat

| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
| 3.x.x | :white_check_mark: |
| 0.x.x | :x: |

## Reporting a Vulnerability
Expand Down
37 changes: 2 additions & 35 deletions deployment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,7 @@ if [ -z "$REPO" ]; then echo "Dorametrix error: REPO is not set! Exiting..." &&
CURRENT_GIT_SHA=$(git log --pretty=format:'%H' -n 1)
echo "ℹ️ CURRENT_GIT_SHA --> $CURRENT_GIT_SHA"

# Get commit ID of last production deployment
LAST_PROD_DEPLOY=$(curl "$ENDPOINT/lastdeployment?product=$REPO_NAME" -H 'Authorization: "$API_KEY"' | jq '.id' -r)

# If no LAST_PROD_DEPLOY is found, then very defensively assume that the first commit is most recent deployment
if [[ -z "$LAST_PROD_DEPLOY" ]] || [[ "$LAST_PROD_DEPLOY" == "null" ]]; then
echo "⚠️ Dorametrix warning: Could not find a value for LAST_PROD_DEPLOY. Setting LAST_PROD_DEPLOY to the value of the first commit."
LAST_PROD_DEPLOY=$(git rev-list HEAD | tail -n 1)
fi
echo "ℹ️ LAST_PROD_DEPLOY --> $LAST_PROD_DEPLOY"

echo "Verifying that commits exist..."
if ! git --no-pager log $LAST_PROD_DEPLOY..$CURRENT_GIT_SHA --decorate=short --pretty=oneline; then
echo "🔥 Dorametrix error: Unable to find the expected commits in working tree! Exiting..."
exit 1
fi

# Get all commits between current work and last production deployment then put result in local TXT file
git log $LAST_PROD_DEPLOY..$CURRENT_GIT_SHA --pretty=format:'{%n ^^^^id^^^^: ^^^^%H^^^^,%n ^^^^timeCreated^^^^: ^^^^%ct^^^^%n },' | sed 's/"/\\"/g' | sed 's/\^^^^/"/g' | sed "$ s/,$//" | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g' | awk 'BEGIN { print("[") } { print($0) } END { print("]") }' >commits.json

# Use TXT output to set variable with list of commits
CHANGES=$(cat commits.json | jq '[.[] | { id: .id, timeCreated: .timeCreated }]')
echo "ℹ️ CHANGES --> $CHANGES"
CHANGES_LENGTH=$(echo $CHANGES | jq '. | length' -r)
echo "ℹ️ CHANGES_LENGTH --> $CHANGES_LENGTH"

# Remove the scratch TXT file
rm commits.json

if [[ $CHANGES_LENGTH -eq 0 ]]; then
echo "🔥 Dorametrix error: No changes detected. Exiting..."
exit 1
fi

# Call Dorametrix and create deployment event with Git changes
curl -X POST $ENDPOINT/event?authorization="$API_KEY" -d '{ "eventType": "deployment", "repo": "'$REPO_NAME'", "changes": '"$CHANGES"' }' -H "Content-Type: application/json"
curl -X POST $ENDPOINT/event?authorization="$API_KEY" -d '{ "eventType": "deployment", "repo": "'$REPO_NAME'", "changeSha": '"$CURRENT_GIT_SHA"' }' -H "Content-Type: application/json"

echo -e "\n✅ Dorametrix deployment script has finished successfully!"
echo -e "\n✅ Dorametrix deployment script has finished successfully!"
2 changes: 1 addition & 1 deletion jest.env.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
process.env.IS_TEST = true;
process.env.IS_TEST = true;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dorametrix",
"description": "Dorametrix is a Node.js-based service that helps you calculate your DORA metrics, by inferring your metrics from events you can create manually or with webhooks",
"version": "2.2.2",
"version": "3.0.0",
"author": "Mikael Vesavuori",
"license": "MIT",
"keywords": [
Expand Down
85 changes: 28 additions & 57 deletions src/domain/services/Dorametrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getDiffInSeconds, prettifyTime } from 'chrono-utils';

import { Dorametrix } from '../../interfaces/Dorametrix';
import { Change } from '../../interfaces/Change';
import { Deployment, DeploymentChange } from '../../interfaces/Deployment';
import { Deployment } from '../../interfaces/Deployment';
import { Incident } from '../../interfaces/Incident';
import { DeploymentResponse } from '../../interfaces/DeploymentResponse';

Expand All @@ -27,32 +27,12 @@ class DorametrixConcrete implements Dorametrix {
* @description Get the commit ID for the last deployment to production.
*/
public getLastDeployment(lastDeployment: Deployment): DeploymentResponse {
if (lastDeployment?.changes) {
const changes: DeploymentChange[] = lastDeployment?.changes;

// Get latest deployment
const deploymentTimes = changes
.map((change) => change.timeCreated)
.sort()
.reverse();
const latestTime = deploymentTimes[0];

// Get the ID of the latest deployment
const matchingChange = changes.filter(
(change: DeploymentChange) => change.timeCreated === latestTime
);

if (matchingChange && matchingChange.length > 0) {
const { id } = matchingChange[0];

// If the timestamp uses a 10-digit format, add zeroes to be in line with how JavaScript does it
const timeCreated = latestTime.length === 10 ? latestTime + '000' : latestTime;

return {
id,
timeCreated
};
}
if (lastDeployment?.id && lastDeployment?.timeCreated) {
const { id, timeCreated } = lastDeployment;
return {
id,
timeCreated
};
}

return {
Expand Down Expand Up @@ -84,51 +64,42 @@ class DorametrixConcrete implements Dorametrix {
}

/**
* @description Get the averaged lead time for a change getting into production (deployment).
* @description Get the median lead time for a change getting into production (deployment).
*/
public getLeadTimeForChanges(changes: Change[], deployments: Deployment[]): string {
if (deployments.length === 0) return '00:00:00:00';

let accumulatedTime = 0;
deployments.forEach(
(deployment: Deployment) => (accumulatedTime += this.calculateLeadTime(deployment, changes))
);
const accumulatedTimes = deployments
.map((deployment: Deployment) => this.calculateLeadTime(deployment, changes))
.sort();

return prettifyTime(accumulatedTime / deployments.length);
const medianPoint =
accumulatedTimes.length < 2 ? 0 : Math.floor(accumulatedTimes.length / 2) - 1;
const medianValue = accumulatedTimes[medianPoint] || 0;

return prettifyTime(medianValue);
}

/**
* @description Calculate the lead time of a change for an individual deployment.
*/
private calculateLeadTime(deployment: Deployment, allChanges: Change[]): number {
const { changes, timeCreated } = deployment;

/**
* Each change might lead to one or more deployments, so go and get each one.
*/
const changeIds = changes.map((change: DeploymentChange) => change.id);
const matches = allChanges
.filter((change: DeploymentChange) => changeIds.includes(change.id))
.map((change: DeploymentChange) => change.timeCreated)
.sort((a: any, b: any) => a.timeCreated - b.timeCreated);
private calculateLeadTime(deployment: Deployment, changes: Change[]): number {
const { changeSha, timeCreated } = deployment;

/**
* Calculate diff between earliest commit timestamp (`firstMatch`) and deployment timestamp (`timeCreated`).
*/
if (matches?.length > 0) {
const firstMatch = matches[0];
const commitTimeCreated = changes
.filter((change: Change) => change.id === changeSha)
.map((change: Change) => change.timeCreated)[0];

if (firstMatch && timeCreated && firstMatch > timeCreated) {
console.warn(
`Unexpected deployment data: firstMatch field is later than timeCreated...Skipping it.\n--> timeCreated: ${firstMatch} firstMatch: ${firstMatch}`
);
return 0;
}
if (!commitTimeCreated) return 0;

return getDiffInSeconds(firstMatch, timeCreated);
if (commitTimeCreated > timeCreated) {
console.warn(
`Unexpected deployment data: Commit timeCreated is later than deployment timeCreated...Skipping it.\n--> Deployment: ${timeCreated} Commit: ${commitTimeCreated}`
);
return 0;
}

return 0;
return getDiffInSeconds(commitTimeCreated, timeCreated);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/domain/valueObjects/Deployment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCurrentDate } from 'chrono-utils';

import { Deployment, DeploymentChange } from '../../interfaces/Deployment';
import { Deployment } from '../../interfaces/Deployment';
import { Event } from '../../interfaces/Event';

import {
Expand All @@ -22,11 +22,11 @@ class DeploymentConcrete {
date: string;
eventType: string;
id: string;
changes: DeploymentChange[];
changeSha: string;
timeCreated: string;

constructor(deploymentEvent: Event) {
const { repo, id, eventType, timeCreated, changes } = deploymentEvent;
const { repo, id, eventType, timeCreated, changeSha } = deploymentEvent;

if (!repo)
throw new MissingRepoNameError(
Expand All @@ -45,7 +45,7 @@ class DeploymentConcrete {
this.date = getCurrentDate(true);
this.eventType = eventType;
this.id = id;
this.changes = changes || [];
this.changeSha = changeSha || '';
this.timeCreated = timeCreated.toString();
}

Expand All @@ -55,7 +55,7 @@ class DeploymentConcrete {
date: this.date,
eventType: this.eventType,
id: this.id,
changes: this.changes,
changeSha: this.changeSha,
timeCreated: this.timeCreated
});
}
Expand Down
25 changes: 12 additions & 13 deletions src/domain/valueObjects/Event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getCurrentDate } from 'chrono-utils';

import { Event, EventType } from '../../interfaces/Event';
import { Change } from '../../interfaces/Change';
import { Parser } from '../../interfaces/Parser';

/**
Expand All @@ -13,17 +12,17 @@ export async function makeEvent(
body: Record<string, any>,
headers: Record<string, any>
): Promise<Event> {
const eventConcrete = new EventConcrete(parser, body, headers);
await EventConcrete.populate(eventConcrete, parser, body, headers);
const eventConcrete = new DorametrixEvent(parser, body, headers);
await DorametrixEvent.populate(eventConcrete, parser, body, headers);
return eventConcrete.getDTO();
}

class EventConcrete {
class DorametrixEvent {
repo = '';
date: string;
eventType = '';
id = '';
changes: Change[] = [];
changeSha = '';
eventTime = '';
timeCreated = '';
timeResolved = '';
Expand All @@ -32,23 +31,23 @@ class EventConcrete {

constructor(parser: Parser, body: Record<string, any>, headers: Record<string, any>) {
this.date = getCurrentDate(true);
EventConcrete.populate(this, parser, body, headers);
DorametrixEvent.populate(this, parser, body, headers);
}

static async populate(
event: EventConcrete,
event: DorametrixEvent,
parser: Parser,
body: Record<string, any>,
headers: Record<string, any>
): Promise<EventConcrete> {
): Promise<DorametrixEvent> {
const eventType = await parser.getEventType({ body, headers });

const repo = parser.getRepoName(body);

event.repo = repo;
event.date = getCurrentDate(true);
event.eventType = eventType;
event.changes = body.changes || [];
event.changeSha = body.changeSha || ''; // TODO: Needed? Is same as some other ID...?

const { id, eventTime, timeCreated, timeResolved, title, message } = await parser.getPayload({
body,
Expand All @@ -69,9 +68,9 @@ class EventConcrete {
parser: Parser,
body: Record<string, any>,
headers: Record<string, any>
): Promise<EventConcrete> {
const event = new EventConcrete(parser, body, headers);
return EventConcrete.populate(event, parser, body, headers);
): Promise<DorametrixEvent> {
const event = new DorametrixEvent(parser, body, headers);
return DorametrixEvent.populate(event, parser, body, headers);
}

public getDTO(): Event {
Expand All @@ -80,7 +79,7 @@ class EventConcrete {
date: this.date,
eventType: this.eventType as EventType,
id: this.id,
changes: this.changes,
changeSha: this.changeSha,
eventTime: this.eventTime,
timeCreated: this.timeCreated,
timeResolved: this.timeResolved,
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/adapters/web/AddEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function handler(
const headers = event.headers;

const repo = createNewDynamoDbRepository();
const parser = await getParser(headers);
const parser = getParser(headers);
const metricEvent = await makeEvent(parser, body, headers);

await createEvent(repo, metricEvent);
Expand Down
Loading

0 comments on commit d4eca99

Please sign in to comment.