Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .github/workflows/phpunit-feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ jobs:
- name: Run the required checks for merge queue groups
run: node --test tests/Unit/MergeGroupTriggersTest.mjs

- name: Report one Shared Server fixtures result on every pull request
run: node --test tests/Unit/ServerParityAggregateTest.mjs

- name: Validate embedded Workflow package authority
run: php scripts/ci/test-workflow-package-authority.php

Expand Down
106 changes: 51 additions & 55 deletions .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
@@ -1,62 +1,9 @@
name: Shared Server fixtures

on:
# Pull requests run without a path filter so that the terminal result always
# reports. The `changes` job applies the push path list below to them.
pull_request:
paths:
- .github/workflows/server-parity.yml
- scripts/conformance/server-parity*
- scripts/conformance/server-parity/**
- tests/Fixtures/ServerParity/**
- tests/Fixtures/ServerParityBaselines/**
- tests/Fixtures/ServerParityNormalization/**
- tests/Unit/ServerParityRunnerTest.mjs
- tests/Unit/ServerParityArtifactSummaryTest.mjs
- tests/Unit/ServerParityQueryObserverTest.mjs
- tests/Unit/ServerParityCodecDeadlineLoadTest.mjs
- tests/Unit/ServerParityCooperativeContractTest.mjs
- tests/Unit/ServerParityChildCancellationContractTest.mjs
- tests/Support/ServerParityChildCancellationObservation.mjs
- tests/Unit/ServerParityScheduleContractTest.mjs
- tests/Support/ServerParityScheduleObservation.mjs
- tests/Fixtures/ServerParityPending/**
- tests/Fixtures/ServerParityExpected/**
- tests/Fixtures/ServerParityProfiles/**
- tests/Unit/ServerParityVisibilityContractTest.mjs
- tests/Unit/ServerParityAdmissionContractTest.mjs
- tests/Unit/ServerParityRoleTokenContractTest.mjs
- tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs
- tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs
- tests/Unit/ServerParitySearchAttributeContractTest.mjs
- tests/Unit/ServerParityStorageTestContractTest.mjs
- tests/Unit/ServerParitySchedulePoliciesContractTest.mjs
- tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityScheduleCatalogContractTest.mjs
- tests/Unit/ServerParityWorkerListingContractTest.mjs
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityOperatorMetricsContractTest.mjs
- tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs
- tests/Unit/ServerParityServiceCatalogContractTest.mjs
- tests/Unit/ServerParitySystemMetricsContractTest.mjs
- tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs
- tests/Unit/ServerParityPrometheusMetricsContractTest.mjs
- tests/Unit/ServerParityPayloadCleanupContractTest.mjs
- tests/Unit/ServerParityRepairPassContractTest.mjs
- tests/Unit/ServerParityClusterInfoContractTest.mjs
- tests/Unit/ServerParityNamespaceDeleteContractTest.mjs
- tests/Unit/ServerParityUpdateValidationContractTest.mjs
- tests/Unit/ServerParityWaitingHistoryContractTest.mjs
- tests/Unit/ServerParityTaskRetryContractTest.mjs
- tests/Unit/ServerParityPublishedPatchContractTest.mjs
- tests/Unit/ServerParityPublishedPatchTransportTest.mjs
- tests/Unit/ServerParityLegacyMarkerContractTest.mjs
- tests/Unit/ServerParityRangeRepairContractTest.mjs
- tests/Unit/ServerParitySdkDirectionContractTest.mjs
- tests/Unit/ServerParitySdkChildDirectionContractTest.mjs
- tests/Unit/ServerParitySdkTimerAuthorContractTest.mjs
- tests/Unit/ServerParitySdkSignalDirectionContractTest.mjs
- tests/Support/ServerParityAdmissionObservation.mjs
- tests/Support/ServerParityVisibilityObservation.mjs
- rust/**
push:
branches: [main]
paths:
Expand Down Expand Up @@ -134,7 +81,27 @@ env:
DW_PARITY_CONTINUE_MEMO_UPSERTS: '1'

jobs:
changes:
name: Shared Server fixture inputs
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.classify.outputs.run }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 2
persist-credentials: false
- name: Decide whether fixture inputs changed
id: classify
env:
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/conformance/server-parity/changes.sh >> "$GITHUB_OUTPUT"

published-sdk-adapters:
needs: changes
if: ${{ needs.changes.outputs.run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 25
env:
Expand Down Expand Up @@ -1883,3 +1850,32 @@ jobs:
path: parity-summary
retention-days: 1
if-no-files-found: error

# The one Shared Server fixtures result. It always runs, including after a
# failure or cancellation, and passes only when every fixture job passed, or
# when no fixture input changed and every fixture job was skipped for that
# reason.
result:
name: Shared Server fixtures result
needs:
- changes
- published-sdk-adapters
- codec-deadline-health
- mysql-execution
- postgres-storage
- execution-and-codec
if: ${{ always() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
sparse-checkout: |
scripts/conformance/server-parity/aggregate.sh
scripts/conformance/server-parity/aggregate.mjs
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Require every fixture job to pass, or every one to be skipped for unchanged inputs
env:
NEEDS: ${{ toJSON(needs) }}
run: bash scripts/conformance/server-parity/aggregate.sh
51 changes: 51 additions & 0 deletions scripts/conformance/server-parity/aggregate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
// The Shared Server fixtures result. NEEDS is the result job's `needs`
// context as JSON.
//
// Passes only when every fixture job passed, or when the classifier reported
// that no fixture input changed and every fixture job was skipped for that
// reason. A failed, cancelled or unexpectedly skipped job fails the result,
// and so does anything that cannot be read.
import {pathToFileURL} from 'node:url';

export function decide(needs) {
if (needs === null || typeof needs !== 'object' || Array.isArray(needs)) {
return {pass: false, reason: 'The needs context is not an object.'};
}
const {changes, ...fixtureJobs} = needs;
const results = Object.entries(fixtureJobs).map(([job, value]) => [job, value?.result]);
const summary = results.map(([job, result]) => `${job}=${result}`).join(', ');
if (changes?.result !== 'success') {
return {pass: false, reason: `The change classifier did not succeed (${changes?.result}).`};
}
if (results.length === 0) {
return {pass: false, reason: 'No fixture jobs reported.'};
}
const run = changes.outputs?.run;
const expected = {true: 'success', false: 'skipped'}[run];
if (expected === undefined) {
return {pass: false, reason: `The change classifier reported no decision (${run}).`};
}
const unexpected = results.filter(([, result]) => result !== expected);
if (unexpected.length > 0) {
return {
pass: false,
reason: `Every fixture job must be ${expected} when run=${run}: ${unexpected.map(([job, result]) => `${job}=${result}`).join(', ')}.`,
};
}
return {
pass: true,
reason: run === 'true' ? `Every fixture job passed: ${summary}.` : `No fixture input changed; every fixture job was skipped: ${summary}.`,
};
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
let decision;
try {
decision = decide(JSON.parse(process.env.NEEDS ?? ''));
} catch (error) {
decision = {pass: false, reason: `The needs context could not be read: ${error.message}`};
}
(decision.pass ? console.log : console.error)(decision.reason);
process.exitCode = decision.pass ? 0 : 1;
}
5 changes: 5 additions & 0 deletions scripts/conformance/server-parity/aggregate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# The Shared Server fixtures result, from the repository root. NEEDS is the
# result job's needs context as JSON. The decision rules live in aggregate.mjs.
set -euo pipefail
exec node scripts/conformance/server-parity/aggregate.mjs
76 changes: 76 additions & 0 deletions scripts/conformance/server-parity/changes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
// Decides whether Shared Server fixtures must run for this event.
//
// Pull requests run the workflow without a path filter, so its terminal
// result always reports. This script applies the workflow's own push path
// filter to the pull request's changes, so one list governs both triggers.
// Every other event runs every fixture job, and so does any pull request
// whose changes cannot be classified.
//
// Writes `run=true` or `run=false` to stdout, for $GITHUB_OUTPUT.
import {execFileSync} from 'node:child_process';
import {readFileSync} from 'node:fs';
import {pathToFileURL} from 'node:url';

// The `paths` list of the workflow's `on.push` trigger.
export function pushPathPatterns(workflowText) {
const lines = workflowText.split('\n');
const on = lines.indexOf('on:');
const push = lines.indexOf(' push:', on);
if (on < 0 || push < 0) throw new Error('The workflow has no on.push trigger');
const patterns = [];
let inPaths = false;
for (const line of lines.slice(push + 1)) {
if (/^ {0,3}\S/.test(line)) break;
if (line === ' paths:') {
inPaths = true;
} else if (inPaths && line.startsWith(' - ')) {
patterns.push(line.slice(' - '.length).trim());
} else if (inPaths) {
break;
}
}
if (patterns.length === 0) throw new Error('The push trigger has no path filter');
return patterns;
}

// GitHub's filter syntax, limited to the forms this workflow uses:
// `**` matches any characters, `*` matches any characters except `/`.
export function patternRegExp(pattern) {
if (!/^[A-Za-z0-9._/*-]+$/.test(pattern)) throw new Error(`Unsupported path filter pattern: ${pattern}`);
const body = pattern.split('**').map(part => part.split('*').map(text => text.replace(/[.]/g, '\\.')).join('[^/]*')).join('.*');
return new RegExp(`^${body}$`);
}

export function matchingFiles(patterns, files) {
const expressions = patterns.map(patternRegExp);
return files.filter(file => expressions.some(expression => expression.test(file)));
}

function classify({event, headSha, workflowPath}) {
if (event !== 'pull_request') return {run: true, reason: `${event} runs every fixture job`};
// The checkout is the pull request's merge commit: base, then head.
const git = (...args) => execFileSync('git', args, {encoding: 'utf8'}).trim();
let files;
try {
const parents = git('rev-list', '--parents', '-n', '1', 'HEAD').split(' ');
if (parents.length !== 3 || parents[2] !== headSha) throw new Error('HEAD is not this pull request\'s merge commit');
files = git('diff', '--name-only', '--no-renames', 'HEAD^1', 'HEAD').split('\n').filter(Boolean);
} catch (error) {
return {run: true, reason: `changes could not be classified (${error.message}); running every fixture job`};
}
const matched = matchingFiles(pushPathPatterns(readFileSync(workflowPath, 'utf8')), files);
return matched.length > 0
? {run: true, reason: `fixture inputs changed: ${matched.join(', ')}`}
: {run: false, reason: `no fixture input among ${files.length} changed files`};
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const decision = classify({
event: process.env.EVENT_NAME ?? '',
headSha: process.env.PR_HEAD_SHA ?? '',
workflowPath: '.github/workflows/server-parity.yml',
});
console.error(decision.reason);
console.log(`run=${decision.run}`);
}
5 changes: 5 additions & 0 deletions scripts/conformance/server-parity/changes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Prints run=true or run=false for the Shared Server fixtures workflow, from
# the repository root. The decision rules live in changes.mjs.
set -euo pipefail
exec node scripts/conformance/server-parity/changes.mjs
6 changes: 3 additions & 3 deletions tests/Unit/MergeGroupTriggersTest.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The Actions checks a GitHub merge queue waits for must run on merge_group.
// Source-only: enabling the queue is a separate repository setting, and its
// Sonar gate and terminal Shared Server fixtures result are separate
// prerequisites (Server #725).
// Source-only: enabling the queue is a separate repository setting. Its Sonar
// gate is a separate prerequisite (Server #725); the terminal Shared Server
// fixtures result is covered by ServerParityAggregateTest.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
Expand Down
Loading
Loading