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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ CLAUDE.md
.claude/
.cursor/
.aider*

# Working specs (implemented, kept for reference)
TODO*/
54 changes: 54 additions & 0 deletions __test__/domain/channel-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,60 @@ describe('ChannelManifest', () => {
});
});

describe('findEntry — match scoring', () => {
it('prefers exact source match over pattern match', () => {
const manifest = ChannelManifest.parse({
documents: [
{
pattern: 'cc-*',
channels: ['public/archive'],
visibility: 'public'
},
{
source: 'sources/cc-51015.adoc',
channels: ['public/standards'],
visibility: 'public'
}
]
});
const policy = manifest.resolve(
mockDoc('sources/cc-51015.adoc', 'cc-51015')
);
expect(policy.channels[0].toString()).toBe('public/standards');
});

it('prefers longer pattern over shorter pattern', () => {
const manifest = ChannelManifest.parse({
documents: [
{ pattern: 'cc-*', channels: ['public/archive'] },
{
pattern: 'cc-5101*',
channels: ['public/standards']
}
]
});
const policy = manifest.resolve(
mockDoc('sources/cc-51015.adoc', 'cc-51015')
);
expect(policy.channels[0].toString()).toBe('public/standards');
});

it('uses pattern match when no source matches', () => {
const manifest = ChannelManifest.parse({
documents: [
{
pattern: 'cc-0*',
channels: ['public/archive']
}
]
});
const policy = manifest.resolve(
mockDoc('sources/cc-0100.adoc', 'cc-0100')
);
expect(policy.channels[0].toString()).toBe('public/archive');
});
});

describe('listAll', () => {
it('returns all entries', () => {
const manifest = ChannelManifest.parse({
Expand Down
33 changes: 33 additions & 0 deletions __test__/input-helper-full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('getInputs', () => {
expect(config.token).toBe('ghp_test123');
expect(config.concurrency).toBe(4);
expect(config.stages).toEqual([]);
expect(config.channels).toEqual([]);
expect(config.extractionFailureThreshold).toBe(0.5);
expect(config.repo).toEqual({ owner: 'owner', repo: 'repo' });
expect(config.workspacePath).toBe('/github/workspace');
Expand Down Expand Up @@ -125,4 +126,36 @@ describe('getInputs', () => {
'Invalid GITHUB_REPOSITORY format'
);
});

it('reads channels input', async () => {
setInput('channels', 'public/standards, members/internal-review');
setInput('token', 'test');

const config = await getInputs();
expect(config.channels).toHaveLength(2);
expect(config.channels[0].toString()).toBe('public/standards');
expect(config.channels[1].toString()).toBe('members/internal-review');
});

it('returns empty channels when not provided', async () => {
setInput('token', 'test');

const config = await getInputs();
expect(config.channels).toEqual([]);
});

it('reads members default-visibility', async () => {
setInput('default-visibility', 'members');
setInput('token', 'test');

const config = await getInputs();
expect(config.defaultVisibility).toBe('members');
});

it('throws for invalid default-visibility', async () => {
setInput('default-visibility', 'invalid');
setInput('token', 'test');

await expect(getInputs()).rejects.toThrow('Invalid default-visibility');
});
});
48 changes: 48 additions & 0 deletions __test__/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { DocumentMetadata } from '../src/domain/document-metadata.js';
import type { ReleaseConfig } from '../src/input-helper.js';
import { ReleasePipeline, type PipelineDependencies } from '../src/pipeline.js';
import { ChannelManifest } from '../src/domain/channel-manifest.js';
import { Channel } from '../src/domain/channel.js';
import { createDefaultRegistry } from '../src/packaging/naming-strategy.js';

function makeDoc(
Expand Down Expand Up @@ -50,6 +51,7 @@ function makeConfig(overrides: Partial<ReleaseConfig> = {}): ReleaseConfig {
repo: { owner: 'test', repo: 'repo' },
concurrency: 4,
stages: [],
channels: [],
extractionFailureThreshold: 0.5,
...overrides
};
Expand Down Expand Up @@ -304,4 +306,50 @@ describe('ReleasePipeline', () => {

expect(mockDiscover).toHaveBeenCalledWith('/workspace/_site');
});

it('channelOverride overrides manifest channels', async () => {
const config = makeConfig();
const { deps, mockDiscover } = createMockDeps({
changedDocs: ['cc-51015']
});
const overrideChannels = [Channel.parse('public/guides')];
deps.channelOverride = overrideChannels;
const docs = [makeDoc('CC 51015')];

mockDiscover.mockResolvedValue(docs);

const pipeline = new ReleasePipeline(config, deps);
const result = await pipeline.execute();

expect(result.released).toHaveLength(1);
expect(deps.publisher.publish).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
overrideChannels
);
});

it('tracks releasedArtifacts with id, tag, url, channels', async () => {
const config = makeConfig();
const { deps, mockDiscover } = createMockDeps({
changedDocs: ['cc-51015']
});
const docs = [makeDoc('CC 51015')];

mockDiscover.mockResolvedValue(docs);

const pipeline = new ReleasePipeline(config, deps);
const result = await pipeline.execute();

expect(result.releasedArtifacts).toHaveLength(1);
expect(result.releasedArtifacts[0].id).toBe('cc-51015');
expect(result.releasedArtifacts[0].url).toBe(
'https://github.com/test/repo/releases/tag/test/ed1'
);
expect(result.releasedArtifacts[0].channels).toEqual(['public/default']);
});
});
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ inputs:
description: 'Comma-separated stages to release (e.g. "published,final-draft"). Empty = all stages.'
required: false
default: ''
channels:
description: 'Override channels for all documents (comma-separated). Empty = use manifest.'
required: false
default: ''
default-visibility:
description: 'Default visibility for documents not listed in release manifest (public or private)'
description: 'Default visibility for documents not listed in release manifest (public, private, or members)'
required: false
default: 'public'
concurrency:
Expand All @@ -56,6 +60,8 @@ outputs:
description: 'JSON array of failed document identifiers'
total-documents:
description: 'Total number of documents processed'
released-artifacts:
description: 'JSON array of { id, tag, url, channels } for released documents'

runs:
using: 'node24'
Expand Down
114 changes: 57 additions & 57 deletions dist/index.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions dist/index.js.map

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions src/domain/channel-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,25 @@ export class ChannelManifest {
sourcePath: string;
id: { toString(): string };
}): ManifestEntry | undefined {
return this.entries.find((e) => {
if (e.source && e.source === doc.sourcePath) return true;
if (e.pattern && minimatch(doc.id.toString(), e.pattern)) return true;
return false;
});
let bestEntry: ManifestEntry | undefined;
let bestScore = 0;

for (const e of this.entries) {
let score = 0;

if (e.source && e.source === doc.sourcePath) {
score = 100;
} else if (e.pattern && minimatch(doc.id.toString(), e.pattern)) {
score = 50 + e.pattern.length;
}

if (score > bestScore) {
bestScore = score;
bestEntry = e;
}
}

return bestEntry;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/filters/manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from '../shared/logger.js';
export async function loadManifest(
sourcePath: string,
fileName: string,
defaultVisibility: 'public' | 'private' = 'public'
defaultVisibility: 'public' | 'private' | 'members' = 'public'
): Promise<ChannelManifest> {
const filePath = join(sourcePath, fileName);

Expand Down
23 changes: 18 additions & 5 deletions src/input-helper.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { getInput } from '@actions/core';
import { resolve } from 'path';
import { Channel } from './domain/channel.js';

export interface ReleaseConfig {
sourcePath: string;
outputDir: string;
releaseConfigFile: string;
workspacePath: string;

defaultVisibility: 'public' | 'private';
defaultVisibility: 'public' | 'private' | 'members';
force: boolean;
includePattern: string;
concurrency: number;
stages: string[];
channels: Channel[];
extractionFailureThreshold: number;
token: string;

Expand All @@ -30,6 +32,7 @@ export async function getInputs(): Promise<ReleaseConfig> {
includePattern: getIncludePattern(),
concurrency: getConcurrency(),
stages: getStages(),
channels: getChannels(),
extractionFailureThreshold: getExtractionFailureThreshold(),
token: getToken(),

Expand Down Expand Up @@ -75,6 +78,16 @@ function getStages(): string[] {
.filter(Boolean);
}

function getChannels(): Channel[] {
const raw = getInput('channels')?.trim();
if (!raw) return [];
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => Channel.parse(s));
}

function getExtractionFailureThreshold(): number {
const raw = getInput('extraction-failure-threshold') || '0.5';
const value = parseFloat(raw);
Expand All @@ -86,14 +99,14 @@ function getExtractionFailureThreshold(): number {
return value;
}

function getDefaultVisibility(): 'public' | 'private' {
function getDefaultVisibility(): 'public' | 'private' | 'members' {
const value = getInput('default-visibility') || 'public';
if (value !== 'public' && value !== 'private') {
if (value !== 'public' && value !== 'private' && value !== 'members') {
throw new Error(
`Invalid default-visibility: ${value}. Must be 'public' or 'private'.`
`Invalid default-visibility: ${value}. Must be 'public', 'private', or 'members'.`
);
}
return value as 'public' | 'private';
return value as 'public' | 'private' | 'members';
}

function getToken(): string {
Expand Down
4 changes: 3 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ async function run(): Promise<void> {
packager: new ZipPackager(),
publisher: new GitHubReleasePublisher(octokit, config.repo),
namingRegistry,
manifest
manifest,
channelOverride: config.channels.length > 0 ? config.channels : undefined
});

const result = await pipeline.execute();
Expand All @@ -64,6 +65,7 @@ async function run(): Promise<void> {
'total-documents',
result.released.length + result.skipped.length + result.failed.length
);
setOutput('released-artifacts', JSON.stringify(result.releasedArtifacts));

if (result.failed.length > 0) {
const failedIds = result.failed
Expand Down
Loading
Loading