Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
[tools]
node = "20"
pnpm = "9.15.0"
kubectl = "latest"
helm = "latest"
kind = "latest"
tilt = "latest"

[env]
_.path = ["./node_modules/.bin"]

[tasks.dev]
description = "Set up kind cluster and run tilt"
run = """
#!/usr/bin/env bash
set -e

CLUSTER_NAME="lfc"
REGISTRY_CONFIG_DIR="/tmp/kind-registry-config/10.96.188.230:5000"

echo "Setting up registry config for containerd..."
mkdir -p "$REGISTRY_CONFIG_DIR"
cat > "$REGISTRY_CONFIG_DIR/hosts.toml" << 'EOF'
server = "http://10.96.188.230:5000"

[host."http://10.96.188.230:5000"]
capabilities = ["pull", "resolve", "push"]
skip_verify = true
EOF

if kind get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo "Kind cluster '$CLUSTER_NAME' already exists"
else
echo "Creating kind cluster '$CLUSTER_NAME'..."
kind create cluster --config sysops/tilt/kind-config.yaml --name "$CLUSTER_NAME"
fi

echo "Switching kubectl context to kind-$CLUSTER_NAME..."
kubectl config use-context "kind-$CLUSTER_NAME"

echo "Starting tilt..."
tilt up
"""

[tasks.down]
description = "Stop tilt (cluster remains)"
run = """
#!/usr/bin/env bash
set -e

echo "Stopping tilt..."
tilt down

echo "Tilt stopped. Kind cluster 'lfc' is still running."
echo "Run 'mise run clean' to delete the cluster."
"""

[tasks.clean]
description = "Delete kind cluster and clean up"
run = """
#!/usr/bin/env bash
set -e

CLUSTER_NAME="lfc"

echo "Stopping tilt (if running)..."
tilt down 2>/dev/null || true

if kind get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo "Deleting kind cluster '$CLUSTER_NAME'..."
kind delete cluster --name "$CLUSTER_NAME"
else
echo "Kind cluster '$CLUSTER_NAME' does not exist"
fi

echo "Cleaning up registry config..."
rm -rf /tmp/kind-registry-config

echo "Cleanup complete."
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2025 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('deploys', (table) => {
table.bigInteger('githubDeploymentId').alter();
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('deploys', (table) => {
table.integer('githubDeploymentId').alter();
});
}
160 changes: 159 additions & 1 deletion src/server/lib/github/__tests__/deployments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import mockRedisClient from 'server/lib/__mocks__/redisClientMock';
mockRedisClient();

import * as client from 'server/lib/github/client';
import * as githubIndex from 'server/lib/github/index';
import {
createGithubDeployment,
createOrUpdateGithubDeployment,
deleteGithubDeployment,
deleteGithubDeploymentAndEnvironment,
deleteGithubEnvironment,
updateDeploymentStatus,
} from 'server/lib/github/deployments';

jest.mock('server/services/globalConfig', () => {
Expand All @@ -40,6 +42,9 @@ jest.mock('server/services/globalConfig', () => {
});

jest.mock('server/lib/github/client');
jest.mock('server/lib/github/index', () => ({
getPullRequest: jest.fn(),
}));
jest.mock('axios');

describe('GitHub Deployment Functions', () => {
Expand Down Expand Up @@ -98,7 +103,7 @@ describe('GitHub Deployment Functions', () => {
await deleteGithubDeploymentAndEnvironment(mockDeploy);

expect(mockDeploy.$fetchGraph).toHaveBeenCalledWith('build.pullRequest.repository');
expect(mockOctokit.request).toHaveBeenCalledTimes(2);
expect(mockOctokit.request).toHaveBeenCalledTimes(3); // markInactive + deleteDeployment + deleteEnvironment
});

test('createGithubDeployment - success', async () => {
Expand Down Expand Up @@ -144,4 +149,157 @@ describe('GitHub Deployment Functions', () => {
`DELETE /repos/${mockDeploy.build.pullRequest.repository.fullName}/deployments/${mockDeploy.githubDeploymentId}`
);
});

test('createOrUpdateGithubDeployment - uses newly created deployment ID for status update', async () => {
const newDeploymentId = 999888;
const mockDeployForIdTest = {
uuid: '1234',
githubDeploymentId: null,
status: 'deployed',
$fetchGraph: jest.fn(),
$query: jest.fn(() => ({
patch: mockPatch,
})),
build: {
status: 'deployed',
pullRequest: {
repository: {
fullName: 'user/repo',
},
pullRequestNumber: 123,
branchName: 'feature-branch',
},
statusMessage: 'Build successful',
},
};

(githubIndex.getPullRequest as jest.Mock).mockResolvedValue({
data: { head: { sha: 'abc123' } },
});

mockOctokit.request.mockImplementation((url) => {
if (url.includes('POST /repos') && url.includes('/deployments') && !url.includes('/statuses')) {
return Promise.resolve({ data: { id: newDeploymentId } });
}
if (url.includes('/statuses')) {
return Promise.resolve({ data: {} });
}
return Promise.resolve({ data: {} });
});

await createOrUpdateGithubDeployment(mockDeployForIdTest);

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.stringContaining(`/deployments/${newDeploymentId}/statuses`),
expect.any(Object)
);
});

test('createGithubDeployment - sets transient_environment to true', async () => {
const deploymentId = '123456';
mockOctokit.request.mockResolvedValue({ data: { id: deploymentId } });

await createGithubDeployment(mockDeploy, 'abc123');

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: expect.objectContaining({
transient_environment: true,
}),
})
);
});

test('updateDeploymentStatus - maps deployed status to success', async () => {
const mockDeployWithStatus = {
...mockDeploy,
status: 'deployed',
build: {
...mockDeploy.build,
status: 'deployed',
},
};
mockOctokit.request.mockResolvedValue({ data: {} });

await updateDeploymentStatus(mockDeployWithStatus, 12345);

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: expect.objectContaining({
state: 'success',
}),
})
);
});

test('updateDeploymentStatus - maps building status to in_progress', async () => {
const mockDeployBuilding = {
...mockDeploy,
status: 'building',
build: {
...mockDeploy.build,
status: 'building',
},
};
mockOctokit.request.mockResolvedValue({ data: {} });

await updateDeploymentStatus(mockDeployBuilding, 12345);

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: expect.objectContaining({
state: 'in_progress',
}),
})
);
});

test('updateDeploymentStatus - maps deploy_failed status to failure', async () => {
const mockDeployFailed = {
...mockDeploy,
status: 'deploy_failed',
build: {
...mockDeploy.build,
status: 'active',
},
};
mockOctokit.request.mockResolvedValue({ data: {} });

await updateDeploymentStatus(mockDeployFailed, 12345);

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: expect.objectContaining({
state: 'failure',
}),
})
);
});

test('updateDeploymentStatus - maps torn_down status to inactive', async () => {
const mockDeployTornDown = {
...mockDeploy,
status: 'torn_down',
build: {
...mockDeploy.build,
status: 'active',
},
};
mockOctokit.request.mockResolvedValue({ data: {} });

await updateDeploymentStatus(mockDeployTornDown, 12345);

expect(mockOctokit.request).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: expect.objectContaining({
state: 'inactive',
}),
})
);
});
});
Loading