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
18 changes: 6 additions & 12 deletions .github/workflows/release-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ name: Release Preview
on:
workflow_dispatch:
inputs:
target_branch:
description: 'Branch to preview release from'
type: choice
options:
- 'next'
- 'master'
required: true
default: 'next'
release_type:
description: 'Release Type'
type: choice
Expand Down Expand Up @@ -38,13 +30,15 @@ permissions:
jobs:
preview:
name: Preview Release
if: github.ref == 'refs/heads/next'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.target_branch }}
ref: next
fetch-depth: 0
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@v4
Expand Down Expand Up @@ -82,7 +76,7 @@ jobs:
--preid ${{ inputs.preid }} \
--no-git-tag-version \
--no-push \
--allow-branch ${{ inputs.target_branch }}
--allow-branch next

- name: Preview Versions (Graduate)
if: ${{ inputs.release_type == 'graduate' }}
Expand All @@ -91,7 +85,7 @@ jobs:
--conventional-graduate \
--no-git-tag-version \
--no-push \
--allow-branch ${{ inputs.target_branch }}
--allow-branch next

- name: Preview Versions (Auto - Conventional Commits)
if: ${{ inputs.release_type == 'auto' }}
Expand All @@ -100,7 +94,7 @@ jobs:
--conventional-commits \
--no-git-tag-version \
--no-push \
--allow-branch ${{ inputs.target_branch }}
--allow-branch next

- name: Generate Version Summary
run: |
Expand Down
23 changes: 18 additions & 5 deletions packages/governance/linters/json-schema-ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,25 @@ export async function lintJsonSchema(
// Warn about unknown $schema versions
if (schema.$schema && typeof schema.$schema === 'string') {
const schemaUri = schema.$schema;
const supportedDrafts = ['draft-04', 'draft-06', 'draft-07', 'draft/2019-09', 'draft/2020-12'];
const isKnown = supportedDrafts.some(
(draft) => schemaUri.includes(draft) || schemaUri.includes(draft.replace('draft-', 'draft/'))
);
let isKnown = false;
try {
const uri = new URL(schemaUri);
const paths = [
'/draft-04/schema',
'/draft-06/schema',
'/draft-07/schema',
'/draft/2019-09/schema',
'/draft/2020-12/schema',
];
isKnown =
['http:', 'https:'].includes(uri.protocol) &&
uri.hostname === 'json-schema.org' &&
paths.includes(uri.pathname);
} catch {
// Malformed identifiers are unrecognized drafts.
}

if (!isKnown && !schemaUri.includes('json-schema.org')) {
if (!isKnown) {
warnings.push({
path: '/$schema',
message: `Unknown schema draft: ${schemaUri}. Validation may be incomplete.`,
Expand Down
63 changes: 63 additions & 0 deletions tests/e2e/22-advanced-security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { afterEach, describe, expect, test } from 'vitest';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { parse } from 'yaml';
import { lintJsonSchema } from '../../packages/governance/linters/json-schema-ajv.js';

const directories: string[] = [];
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});

describe('schema draft URL recognition', () => {
test.each([
['http://json-schema.org/draft-04/schema#', false],
['https://json-schema.org/draft-06/schema#', false],
['https://json-schema.org/draft-07/schema#', false],
['https://json-schema.org/draft/2019-09/schema', false],
['https://json-schema.org/draft/2020-12/schema', false],
['https://json-schema.org.evil.test/draft-07/schema', true],
['https://evil-json-schema.org/draft-07/schema', true],
['https://evil.test/json-schema.org/draft-07/schema', true],
['https://evil.test/?draft-07=json-schema.org', true],
['https://json-schema.org@evil.test/draft-07/schema', true],
['https://json-schema.org/unknown/schema', true],
['https://json-schema.org/draft-07/schema/extra', true],
['ftp://json-schema.org/draft-07/schema', true],
['not-a-url', true],
])('recognizes only supported drafts on the official host: %s', async ($schema, unknown) => {
const directory = mkdtempSync(join(tmpdir(), 'contractual-schema-uri-'));
directories.push(directory);
const path = join(directory, 'schema.json');
writeFileSync(path, JSON.stringify({ $schema, type: 'object' }));
const result = await lintJsonSchema(path, { skipMetaValidation: true, skipStyleRules: true });
expect(result.warnings.some((issue) => issue.rule === 'unknown-draft')).toBe(unknown);
});
});

describe('release-preview trust boundary', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/release-preview.yml', import.meta.url), 'utf8')
);

test('only executes trusted next code, without a caller-selected checkout', () => {
expect(workflow.on.workflow_dispatch.inputs).not.toHaveProperty('target_branch');
expect(workflow.jobs.preview.if).toBe("github.ref == 'refs/heads/next'");
const checkout = workflow.jobs.preview.steps.find((step: { uses?: string }) =>
step.uses?.startsWith('actions/checkout@')
);
expect(checkout.with.ref).toBe('next');
expect(checkout.with['persist-credentials']).toBe(false);
});

test('keeps read-only permissions and does not restore or save caches', () => {
expect(workflow.permissions).toEqual({ contents: 'read' });
for (const step of workflow.jobs.preview.steps) {
expect(step.uses || '').not.toMatch(/^actions\/cache(?:\/|@)/);
expect(step.with?.cache).toBeUndefined();
}
});
});
Loading