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
54 changes: 52 additions & 2 deletions .github/workflows/client-v1-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,66 @@ jobs:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: OpenCoven/sdk
fetch-depth: 1
persist-credentials: false
ref: ${{ vars.CLIENT_V1_CONFORMANCE_VALIDATOR_REVISION }}
path: .phase1-counterparts/sdk-validator
- id: resolve
name: Resolve and verify the producer revision
env:
OPENCOVEN_PRODUCER_REVISION_INPUT: ${{ inputs.producer_revision }}
OPENCOVEN_VALIDATOR_REVISION_INPUT: ${{ inputs.validator_revision }}
OPENCOVEN_PROTECTED_VALIDATOR_REVISION: ${{ vars.CLIENT_V1_CONFORMANCE_VALIDATOR_REVISION }}
OPENCOVEN_DISPATCH_SHA: ${{ github.sha }}
run: |
set -euo pipefail
requested="${OPENCOVEN_PRODUCER_REVISION_INPUT:-}"
if [ -z "$requested" ]; then
requested="$OPENCOVEN_DISPATCH_SHA"
validator_revision="${OPENCOVEN_VALIDATOR_REVISION_INPUT:-}"
protected_validator_revision="${OPENCOVEN_PROTECTED_VALIDATOR_REVISION:-}"
case "$validator_revision" in
*[!0-9a-f]* | "" ) echo 'Validator input revision must be an exact lowercase 40-hex commit.' >&2; exit 1 ;;
esac
if [ "${#validator_revision}" -ne 40 ]; then
echo 'Validator input revision must be an exact lowercase 40-hex commit.' >&2
exit 1
fi
case "$protected_validator_revision" in
*[!0-9a-f]* | "" ) echo 'Protected workflow revision must be an exact lowercase 40-hex commit.' >&2; exit 1 ;;
esac
if [ "${#protected_validator_revision}" -ne 40 ]; then
echo 'Protected workflow revision must be an exact lowercase 40-hex commit.' >&2
exit 1
fi
if [ "$validator_revision" != "$protected_validator_revision" ]; then
echo 'inputs.validator_revision does not match the protected environment validator revision.' >&2
exit 1
fi
requested="$(
node --input-type=module --eval "
import { readFileSync } from 'node:fs';
const lockPath = process.argv[1];
let lock;
try {
lock = JSON.parse(readFileSync(lockPath, 'utf8'));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(\`Validator lock at \${lockPath} is missing or invalid JSON: \${message}\`);
process.exit(1);
}
const commit = lock?.evidenceProducer?.commit;
if (typeof commit !== 'string' || !/^[0-9a-f]{40}$/.test(commit)) {
console.error(
\`Validator lock at \${lockPath} must contain evidenceProducer.commit as an exact lowercase 40-hex commit.\`,
);
process.exit(1);
}
process.stdout.write(commit);
" ".phase1-counterparts/sdk-validator/conformance/client-v1-cross-repository-lock.json"
)"
fi
case "$requested" in
*[!0-9a-f]* | "" ) echo 'Producer revision must be an exact lowercase 40-hex commit.' >&2; exit 1 ;;
Expand All @@ -55,7 +105,7 @@ jobs:
exit 1
fi
if ! git merge-base --is-ancestor "$requested" "$OPENCOVEN_DISPATCH_SHA"; then
echo 'Producer revision is not an ancestor of the dispatch ref; only merged revisions may be validated.' >&2
echo 'Producer revision is not an ancestor of the dispatch ref; only merged revisions may be validated. Supply inputs.producer_revision if the protected validator has not been refreshed yet.' >&2
exit 1
fi
printf 'revision=%s\n' "$requested" >> "$GITHUB_OUTPUT"
Expand Down
2 changes: 1 addition & 1 deletion docs/phase1-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,7 @@ revision authorities can therefore have different workflow hashes:

| File | Bytes | SHA-256 |
| --- | ---: | --- |
| `.github/workflows/client-v1-conformance.yml` | 180,386 | `be7f36a7ce1b3dd6834b565e33825c5af410c49ed0e193ffaffda91ef79125b3` |
| `.github/workflows/client-v1-conformance.yml` | 183,188 | `32349d3f2b3132bea44985693b8a2770f0c057b215e54eedb742613e6d1bb935` |
| `scripts/contract-canary.mjs` | 40,618 | `a4c2fe0a5eb6a5ff4653de5374c34c0fb46907c6806a5d23b86d8b37206ef958` |
| `scripts/executable-resolution.mjs` | 9,154 | `31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430` |
| `scripts/owned-temp-directory.mjs` | 7,762 | `95f546ef9ed614f2a0f55d356ddfc54c943fc53b595b4eebebfcbd4db68e5c0b` |
Expand Down
44 changes: 41 additions & 3 deletions src/client-v1-conformance-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execFileSync, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { dirname, relative, resolve } from 'node:path';
Expand Down Expand Up @@ -703,6 +703,7 @@ function runProducerResolve(
repository: string,
dispatchSha: string,
requested: string | undefined,
validatorRevision = 'a'.repeat(40),
): ResolveOutcome {
const outputPath = resolve(repository, 'github-output');
writeFileSync(outputPath, '');
Expand All @@ -714,6 +715,8 @@ function runProducerResolve(
...process.env,
GITHUB_OUTPUT: outputPath,
OPENCOVEN_DISPATCH_SHA: dispatchSha,
OPENCOVEN_VALIDATOR_REVISION_INPUT: validatorRevision,
OPENCOVEN_PROTECTED_VALIDATOR_REVISION: validatorRevision,
...(requested === undefined ? {} : { OPENCOVEN_PRODUCER_REVISION_INPUT: requested }),
},
});
Expand Down Expand Up @@ -752,15 +755,28 @@ describe('producer revision ancestry gate', () => {
git('-c', 'commit.gpgsign=false', 'commit', '--quiet', '--allow-empty', '-m', 'unmerged');
const unmerged = git('rev-parse', 'HEAD');
git('checkout', '--quiet', 'main');
mkdirSync(resolve(repository, '.phase1-counterparts', 'sdk-validator', 'conformance'), {
recursive: true,
});
writeFileSync(
resolve(
repository,
'.phase1-counterparts',
'sdk-validator',
'conformance',
'client-v1-cross-repository-lock.json',
),
`${JSON.stringify({ evidenceProducer: { commit: merged } })}\n`,
);

afterAll(() => {
rmSync(repository, { force: true, recursive: true });
});

test('defaults to the dispatch tip when no revision is requested', () => {
test('defaults to the validator-locked evidence producer commit when no revision is requested', () => {
const outcome = runProducerResolve(script, repository, tip, undefined);
expect(outcome.status).toBe(0);
expect(outcome.revision).toBe(tip);
expect(outcome.revision).toBe(merged);
});

test('accepts an exact merged ancestor of the dispatch tip', () => {
Expand Down Expand Up @@ -809,6 +825,28 @@ describe('producer revision ancestry gate', () => {
expect(outcome.revision).toBe('');
expect(outcome.stderr).toContain('not a commit in this repository');
});

test('refuses when validator input revision does not match the protected validator revision', () => {
const outputPath = resolve(repository, 'github-output-mismatch');
writeFileSync(outputPath, '');
const result = spawnSync('bash', ['-c', script], {
cwd: repository,
encoding: 'utf8',
timeout: 20_000,
env: {
...process.env,
GITHUB_OUTPUT: outputPath,
OPENCOVEN_DISPATCH_SHA: tip,
OPENCOVEN_VALIDATOR_REVISION_INPUT: 'a'.repeat(40),
OPENCOVEN_PROTECTED_VALIDATOR_REVISION: 'b'.repeat(40),
},
});
expect(result.status).toBe(1);
expect(result.stderr ?? '').toContain(
'inputs.validator_revision does not match the protected environment validator revision.',
);
expect(readFileSync(outputPath, 'utf8')).toBe('');
});
});

describe.skipIf(!validatorAvailable)('protected client-v1 conformance workflow', () => {
Expand Down
Loading