Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/release-e2e-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
CI: 'true'
PLAYWRIGHT_RELEASE_MODE: report
PLAYWRIGHT_RELEASE_REF: ${{ steps.prepare.outputs.ui_sha }}
PLAYWRIGHT_RELEASE_TEST_REF: ${{ steps.prepare.outputs.tests_sha }}
PLAYWRIGHT_RELEASE_VERSION: ${{ steps.prepare.outputs.playwright_version }}
PLAYWRIGHT_RELEASE_OUTPUT: ${{ github.workspace }}/ui
- name: Sanitize report files
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
description: Optional expected full YDB commit SHA (leave blank to resolve from tag)
required: false
type: string
tests_sha:
description: Optional full UI repository commit SHA for tests (defaults to the embedded UI revision)
required: false
type: string

permissions:
contents: read
Expand All @@ -23,6 +27,7 @@ jobs:
timeout-minutes: 5
outputs:
ui_sha: ${{ steps.release.outputs.ui_sha }}
tests_sha: ${{ steps.release.outputs.tests_sha }}
playwright_version: ${{ steps.release.outputs.playwright_version }}
steps:
- name: Require main workflow revision
Expand Down Expand Up @@ -50,6 +55,7 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
YDB_TAG: ${{ inputs.ydb_tag }}
YDB_SHA: ${{ inputs.ydb_sha }}
TESTS_SHA: ${{ inputs.tests_sha }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
- uses: actions/upload-artifact@v4
if: always()
Expand Down Expand Up @@ -101,6 +107,7 @@ jobs:
PLAYWRIGHT_APP_BACKEND: http://localhost:8765
PLAYWRIGHT_VIDEO: retain-on-failure
PLAYWRIGHT_RELEASE_REF: ${{ needs.resolve.outputs.ui_sha }}
PLAYWRIGHT_RELEASE_TEST_REF: ${{ needs.resolve.outputs.tests_sha }}
PLAYWRIGHT_RELEASE_VERSION: ${{ needs.resolve.outputs.playwright_version }}
PLAYWRIGHT_RELEASE_OUTPUT: ${{ github.workspace }}/ui
- name: Collect diagnostics and sanitize artifacts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('release Playwright container boundary', () => {
PATH: `${directory}:${process.env.PATH}`,
CAPTURE: path.join(directory, 'arguments.json'),
PLAYWRIGHT_RELEASE_REF: 'a'.repeat(40),
PLAYWRIGHT_RELEASE_TEST_REF: '',
PLAYWRIGHT_RELEASE_VERSION: '1.58.0',
PLAYWRIGHT_RELEASE_MODE: 'test',
PLAYWRIGHT_RELEASE_OUTPUT: path.join(directory, 'output'),
Expand All @@ -37,6 +38,7 @@ describe('release Playwright container boundary', () => {

test.each([
['test', 'http://localhost:8765'],
['override', 'http://localhost:8765'],
['report', ''],
['local', 'https://localhost:8765'],
])('sets safe mounts and credentials in %s mode', (mode, backend) => {
Expand All @@ -45,7 +47,8 @@ describe('release Playwright container boundary', () => {
env: {
...env,
PLAYWRIGHT_RELEASE_REF: mode === 'local' ? '' : env.PLAYWRIGHT_RELEASE_REF,
PLAYWRIGHT_RELEASE_MODE: mode,
PLAYWRIGHT_RELEASE_MODE: mode === 'override' ? 'test' : mode,
PLAYWRIGHT_RELEASE_TEST_REF: mode === 'override' ? 'b'.repeat(40) : '',
PLAYWRIGHT_APP_BACKEND: backend,
},
});
Expand All @@ -65,6 +68,9 @@ describe('release Playwright container boundary', () => {
expect(mounts).toContain(`${output}/playwright-artifacts:/work/playwright-artifacts`);
expect(mounts).not.toContain(`${root}:/work`);
expect(args).toContain(`PLAYWRIGHT_RELEASE_REF=${'a'.repeat(40)}`);
expect(args).toContain(
`PLAYWRIGHT_RELEASE_TEST_REF=${(mode === 'override' ? 'b' : 'a').repeat(40)}`,
);
}
expect(mounts.some((mount) => mount.includes('docker.sock'))).toBe(false);
expect(args.some((arg) => arg.includes('must-not-enter-container'))).toBe(false);
Expand All @@ -78,15 +84,18 @@ describe('release Playwright container boundary', () => {
}
});

test('rejects an unpinned release ref before starting Docker', () => {
expect(() =>
execFileSync('bash', [runner], {
cwd: root,
env: {...env, PLAYWRIGHT_RELEASE_REF: 'main', PLAYWRIGHT_APP_BACKEND: ''},
stdio: 'pipe',
}),
).toThrow('release mode requires a commit SHA');
});
test.each(['PLAYWRIGHT_RELEASE_REF', 'PLAYWRIGHT_RELEASE_TEST_REF'])(
'rejects an unpinned %s before starting Docker',
(variable) => {
expect(() =>
execFileSync('bash', [runner], {
cwd: root,
env: {...env, [variable]: 'main', PLAYWRIGHT_APP_BACKEND: ''},
stdio: 'pipe',
}),
).toThrow('release mode requires a commit SHA');
},
);

test('rejects an external frontend before starting release Docker', () => {
const result = spawnSync('bash', [runner], {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const provenance = {
ydb_sha: 'a'.repeat(40),
image_digest: `sha256:${'b'.repeat(64)}`,
frontend_mode: 'npm-start',
ui_sha: 'd'.repeat(40),
};
const image = {
Id: 'sha256:container-image',
Expand Down Expand Up @@ -114,10 +115,16 @@ test('recovers completed failed tests while the parent run is still running', as
'failed',
);
const ui = {ui_sha: 'd'.repeat(40), playwright_version: '1.58.0', workflow_sha: workflowSha};
expect(prepareReport(ui, source)).toEqual(ui);
expect(prepareReport(ui, source)).toEqual({...ui, tests_sha: ui.ui_sha});
const override = {...ui, tests_sha: 'f'.repeat(40)};
expect(prepareReport(override, source)).toEqual(override);
const summary = summarize(report, override, context).summary;
expect(summary).toContain(`UI: ${ui.ui_sha}`);
expect(summary).toContain(`Tests: ${override.tests_sha}`);
for (const invalid of [
undefined,
{...ui, ui_sha: 'main'},
{...ui, tests_sha: 'main'},
{...ui, playwright_version: 'latest'},
{...ui, workflow_sha: 'e'.repeat(40)},
]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ test('starts the release-version frontend while keeping local-ydb as its backend
expect(workflow).not.toContain('PLAYWRIGHT_BASE_URL:');
expect(workflow).toContain('PLAYWRIGHT_APP_BACKEND: http://localhost:8765');
expect(workflow).toContain('PLAYWRIGHT_RELEASE_REF: ${{ needs.resolve.outputs.ui_sha }}');
expect(workflow).toContain(
'PLAYWRIGHT_RELEASE_TEST_REF: ${{ needs.resolve.outputs.tests_sha }}',
);
expect(reportWorkflow).toContain(
'PLAYWRIGHT_RELEASE_TEST_REF: ${{ steps.prepare.outputs.tests_sha }}',
);
});

test('forwards backend routes and streaming without rewriting UI paths', async () => {
Expand Down
26 changes: 25 additions & 1 deletion .github/workflows/scripts/__tests__/resolve-release-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ function fixture(version = '18.1.0') {
JSON.stringify({packages: {'node_modules/@playwright/test': {version: '1.58.0'}}}),
),
};
const run = (options: {ydbTag: string; ydbSha?: string; workflowSha: string} = input) =>
const run = (
options: {ydbTag: string; ydbSha?: string; testsSha?: string; workflowSha: string} = input,
) =>
resolveRelease(
options,
async (path: string) => {
Expand All @@ -46,13 +48,35 @@ test.each(['18.1.0', '15.6.0-hotfix.1'])(
ydb_sha: input.ydbSha,
ui_version: version,
ui_sha: uiSha,
tests_sha: uiSha,
playwright_version: '1.58.0',
image_digest: imageDigest,
frontend_mode: 'npm-start',
});
},
);

test('selects test dependencies without changing the release UI identity', async () => {
const {responses, run} = fixture();
const testsSha = 'e'.repeat(40);
responses[`${uiRepo}/commits/${testsSha}`] = {sha: testsSha};
responses[`${uiRepo}/contents/package-lock.json?ref=${testsSha}`] = file(
JSON.stringify({packages: {'node_modules/@playwright/test': {version: '1.59.0'}}}),
);
await expect(run({...input, testsSha})).resolves.toMatchObject({
ui_sha: uiSha,
ui_version: '18.1.0',
tests_sha: testsSha,
playwright_version: '1.59.0',
});
await expect(run({...input, testsSha: ''})).resolves.toMatchObject({tests_sha: uiSha});
responses[`${uiRepo}/commits/${testsSha}`] = {sha: uiSha};
await expect(run({...input, testsSha})).rejects.toThrow('Test revision');
responses[`${uiRepo}/commits/${testsSha}`] = new Error('Commit not found');
await expect(run({...input, testsSha})).rejects.toThrow('Commit not found');
await expect(run({...input, testsSha: 'main'})).rejects.toThrow('tests_sha');
});

test.each([undefined, ''])(
'resolves the tag commit when the expected SHA is %p',
async (ydbSha) => {
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/scripts/release-e2e-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ function prepareReport(provenance, source) {
if (
!provenance ||
!/^[a-f0-9]{40}$/.test(provenance.ui_sha || '') ||
!/^[a-f0-9]{40}$/.test(provenance.tests_sha ?? provenance.ui_sha) ||
!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(provenance.playwright_version || '') ||
provenance.workflow_sha !== source.source_workflow_sha
) {
throw new Error('Missing or invalid provenance for the source release run');
}
return provenance;
return {...provenance, tests_sha: provenance.tests_sha ?? provenance.ui_sha};
}

function collectReports(directory, destination) {
Expand Down Expand Up @@ -125,7 +126,9 @@ function sanitizeArtifacts(directory) {
}

function summarize(report, provenance, {shards, jobs, artifacts, merge}) {
const frontend = provenance ? `Frontend: ${provenance.frontend_mode ?? 'image'}.\n\n` : '';
const frontend = provenance
? `Frontend: ${provenance.frontend_mode ?? 'image'}. UI: ${provenance.ui_sha}. Tests: ${provenance.tests_sha ?? provenance.ui_sha}.\n\n`
: '';
const problems = [];
if (shards !== 8) {
problems.push(`Reports received from ${shards}/8 shards`);
Expand Down Expand Up @@ -191,7 +194,7 @@ async function main() {
);
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`ui_sha=${provenance.ui_sha}\nplaywright_version=${provenance.playwright_version}\n`,
`ui_sha=${provenance.ui_sha}\ntests_sha=${provenance.tests_sha}\nplaywright_version=${provenance.playwright_version}\n`,
);
} else if (command === 'verify') {
const provenance = JSON.parse(fs.readFileSync(args[0], 'utf8'));
Expand Down
37 changes: 26 additions & 11 deletions .github/workflows/scripts/resolve-release-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,21 @@ async function resolveImageDigest(tag) {
return digest;
}

async function getPlaywrightVersion(testsSha, github) {
const lockfile = JSON.parse(
decodeFile(
await github(`ydb-platform/ydb-embedded-ui/contents/package-lock.json?ref=${testsSha}`),
),
);
const version = lockfile.packages?.['node_modules/@playwright/test']?.version;
if (!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version || '')) {
throw new Error('Test lockfile has no valid Playwright version');
}
return version;
}

async function resolveRelease(
{ydbTag, ydbSha: expectedYdbSha, workflowSha},
{ydbTag, ydbSha: expectedYdbSha, testsSha: requestedTestsSha, workflowSha},
github = readGithub,
digest = resolveImageDigest,
) {
Expand All @@ -71,6 +84,9 @@ async function resolveRelease(
if ((expectedYdbSha && !SHA_PATTERN.test(expectedYdbSha)) || !SHA_PATTERN.test(workflowSha)) {
throw new Error('YDB and workflow revisions must be full commit SHAs');
}
if (requestedTestsSha && !SHA_PATTERN.test(requestedTestsSha)) {
throw new Error('tests_sha must be a full commit SHA');
}
const commit = await github(`ydb-platform/ydb/commits/${encodeURIComponent(ydbTag)}`);
const ydbSha = commit.sha;
if (!SHA_PATTERN.test(ydbSha)) {
Expand All @@ -94,17 +110,14 @@ async function resolveRelease(
if (uiPackage.version !== uiVersion) {
throw new Error(`UI package version ${uiPackage.version} does not match ${uiVersion}`);
}
const lockfile = JSON.parse(
decodeFile(
await github(
`ydb-platform/ydb-embedded-ui/contents/package-lock.json?ref=${uiCommit.sha}`,
),
),
);
const playwrightVersion = lockfile.packages?.['node_modules/@playwright/test']?.version;
if (!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(playwrightVersion || '')) {
throw new Error('UI lockfile has no valid Playwright version');
const testsSha = requestedTestsSha || uiCommit.sha;
if (testsSha !== uiCommit.sha) {
const testsCommit = await github(`ydb-platform/ydb-embedded-ui/commits/${testsSha}`);
if (testsCommit.sha !== testsSha) {
throw new Error('Test revision does not match tests_sha');
}
}
const playwrightVersion = await getPlaywrightVersion(testsSha, github);
const imageDigest = await digest(ydbTag);
if (!DIGEST_PATTERN.test(imageDigest)) {
throw new Error('Invalid image digest');
Expand All @@ -114,6 +127,7 @@ async function resolveRelease(
ydb_sha: ydbSha,
ui_version: uiVersion,
ui_sha: uiCommit.sha,
tests_sha: testsSha,
frontend_mode: 'npm-start',
playwright_version: playwrightVersion,
image: `${IMAGE_REPOSITORY}:${ydbTag}`,
Expand All @@ -127,6 +141,7 @@ if (require.main === module) {
resolveRelease({
ydbTag: process.env.YDB_TAG,
ydbSha: process.env.YDB_SHA,
testsSha: process.env.TESTS_SHA,
workflowSha: process.env.WORKFLOW_SHA,
})
.then((provenance) => {
Expand Down
Loading
Loading