Skip to content
Draft
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
90 changes: 78 additions & 12 deletions packages/synthetic-chain/src/cli/dockerfileGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
encodeUpgradeInfo,
imageNameForProposal,
isPassed,
ProposalRange,
type ProposalRange,
type CoreEvalPackage,
type ParameterChangePackage,
type ProposalInfo,
Expand All @@ -17,7 +17,7 @@ const syntaxPragma = '# syntax=docker/dockerfile:1.7-labs';
/**
* Templates for Dockerfile stages
*/
const stage = {
export const stage = {
/**
* Prepare an upgrade from ag0, start of the chain
*/
Expand Down Expand Up @@ -67,20 +67,25 @@ FROM ghcr.io/agoric/agoric-3-proposals:use-${proposalName} as use-${proposalName
proposalName,
upgradeInfo,
releaseNotes,
sdkImageTag,
}: SoftwareUpgradePackage,
lastProposal: ProposalInfo,
currentSdkImageTag: string,
) {
const skipProposalValidation = !releaseNotes;
return `
# PREPARE ${proposalName}

# upgrading to ${planName}
FROM use-${lastProposal.proposalName} as prepare-${proposalName}
FROM ghcr.io/agoric/agoric-sdk:${currentSdkImageTag} as prepare-${proposalName}
ENV \
UPGRADE_TO=${planName} \
UPGRADE_INFO=${JSON.stringify(encodeUpgradeInfo(upgradeInfo))} \
SKIP_PROPOSAL_VALIDATION=${skipProposalValidation}

# Copy chain state from previous layer
COPY --from=use-${lastProposal.proposalName} /root/.agoric /root/.agoric

${createCopyCommand(
['host', 'node_modules', '**/.yarn/install-state.gz', 'test', 'test.sh'],
`./proposals/${path}`,
Expand Down Expand Up @@ -131,6 +136,7 @@ ${createCopyCommand(
)}
RUN --mount=type=cache,target=/root/.yarn ./install_deps.sh ${path}

# Copy chain state from previous PREPARE stage
COPY --from=prepare-${proposalName} /root/.agoric /root/.agoric

SHELL ["/bin/bash", "-c"]
Expand All @@ -144,10 +150,14 @@ RUN ./run_execute.sh ${planName}
EVAL(
{ path, proposalName }: CoreEvalPackage | ParameterChangePackage,
lastProposal: ProposalInfo,
currentSdkImageTag: string,
) {
return `
# EVAL ${proposalName}
FROM use-${lastProposal.proposalName} as eval-${proposalName}
FROM ghcr.io/agoric/agoric-sdk:${currentSdkImageTag} as eval-${proposalName}

# Copy chain state from previous layer
COPY --from=use-${lastProposal.proposalName} /root/.agoric /root/.agoric

${createCopyCommand(
['host', 'node_modules', '**/.yarn/install-state.gz', 'test', 'test.sh'],
Expand Down Expand Up @@ -179,12 +189,19 @@ RUN ./run_eval.sh ${path}
*
* - Perform any mutations that should be part of chain history
*/
USE({ path, proposalName, type }: ProposalInfo) {
USE(proposal: ProposalInfo, currentSdkImageTag: string) {
const { path, proposalName, type } = proposal;
const previousStage =
type === 'Software Upgrade Proposal' ? 'execute' : 'eval';

return `
# USE ${proposalName}
FROM ${previousStage}-${proposalName} as use-${proposalName}
FROM ghcr.io/agoric/agoric-sdk:${currentSdkImageTag} as use-${proposalName}

LABEL agoric.sdk-image-tag="${currentSdkImageTag}"

# Copy chain state from previous stage
COPY --from=${previousStage}-${proposalName} /root/.agoric /root/.agoric

WORKDIR /usr/src/upgrade-test-scripts

Expand All @@ -207,10 +224,13 @@ ENTRYPOINT ./start_agd.sh
*
* Needs to be an image to have access to the SwingSet db. run it with `docker run --rm` to not make the container ephemeral.
*/
TEST({ path, proposalName }: ProposalInfo) {
TEST({ path, proposalName }: ProposalInfo, currentSdkImageTag: string) {
return `
# TEST ${proposalName}
FROM use-${proposalName} as test-${proposalName}
FROM ghcr.io/agoric/agoric-sdk:${currentSdkImageTag} as test-${proposalName}

# Copy chain state from USE stage
COPY --from=use-${proposalName} /root/.agoric /root/.agoric

# Previous stages copied excluding test files (see COPY above). It would be good
# to copy only missing files, but there may be none. Fortunately, copying extra
Expand Down Expand Up @@ -282,9 +302,20 @@ export function writeDockerfile(range: ProposalRange) {
const blocks: string[] = [syntaxPragma];

let previousProposal = range.previousProposal;
let currentSdkImageTag = 'latest'; // Default SDK image tag

if (previousProposal) {
blocks.push(stage.RESUME(previousProposal.proposalName));
// TODO: Extract SDK image tag from existing image when resuming
// For now, we'll use the first proposal's SDK image tag if available
const firstUpgradeProposal = range.proposals.find(
p => p.type === 'Software Upgrade Proposal',
) as SoftwareUpgradePackage | undefined;
if (firstUpgradeProposal) {
currentSdkImageTag = firstUpgradeProposal.sdkImageTag;
}
}

for (const proposal of range.proposals) {
// UNTIL region support https://github.com/microsoft/vscode-docker/issues/230
blocks.push(
Expand All @@ -294,11 +325,22 @@ export function writeDockerfile(range: ProposalRange) {
switch (proposal.type) {
case '/agoric.swingset.CoreEvalProposal':
case '/cosmos.params.v1beta1.ParameterChangeProposal':
blocks.push(stage.EVAL(proposal, previousProposal!));
blocks.push(
stage.EVAL(proposal, previousProposal!, currentSdkImageTag),
);
break;
case 'Software Upgrade Proposal':
// Update SDK image tag for upgrade proposals
currentSdkImageTag = proposal.sdkImageTag;
if (previousProposal) {
blocks.push(stage.PREPARE(proposal, previousProposal));
// For PREPARE, use the previous SDK image tag, not the new one
const previousSdkImageTag = getPreviousSdkImageTag(
previousProposal,
range.proposals,
);
blocks.push(
stage.PREPARE(proposal, previousProposal, previousSdkImageTag),
);
} else {
// handle the first proposal of the chain specially
blocks.push(
Expand All @@ -315,8 +357,8 @@ export function writeDockerfile(range: ProposalRange) {

// The stages must be output in dependency order because if the builder finds a FROM
// that it hasn't built yet, it will search for it in the registry. But it won't be there!
blocks.push(stage.USE(proposal));
blocks.push(stage.TEST(proposal));
blocks.push(stage.USE(proposal, currentSdkImageTag));
blocks.push(stage.TEST(proposal, currentSdkImageTag));
previousProposal = proposal;
}

Expand All @@ -331,3 +373,27 @@ export function writeDockerfile(range: ProposalRange) {
const contents = blocks.join('\n');
fs.writeFileSync('Dockerfile', contents);
}

/**
* Get the SDK image tag that should be used for a given proposal's base image
*/
function getPreviousSdkImageTag(
previousProposal: ProposalInfo,
allProposals: ProposalInfo[],
): string {
// Find the most recent upgrade proposal before the current one
const previousIndex = allProposals.findIndex(
p => p.proposalName === previousProposal.proposalName,
);

// Look backwards for the last upgrade proposal
for (let i = previousIndex; i >= 0; i--) {
const prop = allProposals[i];
if (prop.type === 'Software Upgrade Proposal') {
return prop.sdkImageTag;
}
}

// Default fallback
return 'latest';
}
76 changes: 76 additions & 0 deletions packages/synthetic-chain/test/test-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
type ProposalInfo,
compareProposalDirNames,
imageNameForProposal,
type SoftwareUpgradePackage,
} from '../src/cli/proposals.js';
import { stage } from '../src/cli/dockerfileGen.js';

test('compareProposalDirNames', t => {
const inputs = [
Expand Down Expand Up @@ -53,3 +55,77 @@ test('imageNameForProposal', t => {
target: 'test-foo',
});
});

test('EXECUTE stage uses agoric-sdk base and copies from prepare stage', t => {
const upgradeProposal: SoftwareUpgradePackage = {
type: 'Software Upgrade Proposal',
path: '65:upgrade-13',
proposalName: 'upgrade-13',
proposalIdentifier: '65',
sdkImageTag: '39',
planName: 'agoric-upgrade-13',
releaseNotes:
'https://github.com/Agoric/agoric-sdk/releases/tag/agoric-upgrade-13',
};

const executeStage = stage.EXECUTE(upgradeProposal);

// Verify that the stage uses the correct agoric-sdk base image
t.true(executeStage.includes('FROM ghcr.io/agoric/agoric-sdk:39'));

// Verify that it copies chain state from the PREPARE stage
t.true(
executeStage.includes(
'COPY --from=prepare-upgrade-13 /root/.agoric /root/.agoric',
),
);

// Verify it's the PREPARE stage (not USE stage)
t.true(
executeStage.includes('# Copy chain state from previous PREPARE stage'),
);
});

test('USE stage uses agoric-sdk base and includes SDK image tag label', t => {
const upgradeProposal: SoftwareUpgradePackage = {
type: 'Software Upgrade Proposal',
path: '65:upgrade-13',
proposalName: 'upgrade-13',
proposalIdentifier: '65',
sdkImageTag: '39',
planName: 'agoric-upgrade-13',
releaseNotes:
'https://github.com/Agoric/agoric-sdk/releases/tag/agoric-upgrade-13',
};

const useStage = stage.USE(upgradeProposal, '39');

// Verify that the USE stage uses the correct agoric-sdk base
t.true(useStage.includes('FROM ghcr.io/agoric/agoric-sdk:39'));

// Verify that the USE stage includes the SDK image tag label
t.true(useStage.includes('LABEL agoric.sdk-image-tag="39"'));

// Verify that it copies from the execute stage
t.true(
useStage.includes(
'COPY --from=execute-upgrade-13 /root/.agoric /root/.agoric',
),
);
});

test('USE stage includes SDK image tag for all proposal types', t => {
const coreEvalProposal: ProposalInfo = {
type: '/agoric.swingset.CoreEvalProposal',
path: '92:reset-psm-mintlimit',
proposalName: 'reset-psm-mintlimit',
proposalIdentifier: '92',
source: 'subdir',
};

const useStage = stage.USE(coreEvalProposal, '56');

// Verify that all proposals now include SDK image tag labels
t.true(useStage.includes('LABEL agoric.sdk-image-tag="56"'));
t.true(useStage.includes('FROM ghcr.io/agoric/agoric-sdk:56'));
});