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
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This action sets up the Metanorma toolchain and adds the command-line tools to t

## What's new

- **Extra flavors support**: Install additional metanorma flavor gems (both public and private) in a single step - consolidates functionality from the separate `setup-flavors` action
- **Private flavor support**: Built-in GitHub Packages authentication for private flavors (bsi, nist, plateau) via `github-packages-token` input
- **Idempotent installation**: Automatically skips redundant installations when Metanorma is already installed with the same configuration - saves time in workflows that may call the action multiple times
- **YAML-based version registry**: Fetches version data directly from [metanorma/versions](https://github.com/metanorma/versions) repository YAML files - no Ruby/Bundler dependencies
- **Pre-built Gemfile.lock integration**: Automatically uses pre-tested Gemfile.lock files from [metanorma/versions](https://github.com/metanorma/versions) for deterministic, tested dependency resolution
Expand Down Expand Up @@ -60,6 +62,15 @@ This action sets up the Metanorma toolchain and adds the command-line tools to t
# When true and a specific version is requested, uses pre-built lock files
# When false, respects existing Gemfile.lock in workspace
use-prebuilt-locks: 'true' # optional, default is 'true'

# Space-separated list of extra flavor gems to install
# Public flavors: iso, ietf, ribose, cc, plateau, etc. (from RubyGems)
# Private flavors: bsi, nist (require github-packages-token)
extra-flavors: '' # optional, default is ''

# GitHub token to access private packages at rubygems.pkg.github.com/metanorma
# Required for private flavors: bsi, nist
github-packages-token: '' # optional, default is ''
```
<!-- end usage -->

Expand Down Expand Up @@ -349,6 +360,95 @@ To install pre-release versions from Chocolatey:
choco-prerelease: 'true'
```

## Installing Extra Flavors

The action can install additional metanorma flavor gems beyond the default CLI installation. This replaces the separate `actions-mn/setup-flavors` action.

### Public Flavors

Public flavors are available on RubyGems and don't require authentication:

```yaml
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'

- uses: actions-mn/setup@v1
with:
installation-method: 'gem'
extra-flavors: 'iso ietf ribose'
```

### Private Flavors

Private flavors (bsi, nist) are hosted on GitHub Packages and require authentication:

```yaml
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'

- uses: actions-mn/setup@v1
with:
installation-method: 'gem'
extra-flavors: 'bsi nist'
github-packages-token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
```

### Private Flavors List

| Flavor | Description | Requires Token |
|--------|-------------|----------------|
| `bsi` | British Standards Institution | Yes |
| `nist` | National Institute of Standards and Technology | Yes |
| `iso` | International Organization for Standardization | No |
| `ietf` | Internet Engineering Task Force | No |
| `ribose` | Ribose flavor | No |
| `cc` | CalConnect (formerly csd) | No |
| `plateau` | Plateau flavor | No |

### Mixed Public and Private Flavors

You can install both public and private flavors together:

```yaml
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'

- uses: actions-mn/setup@v1
with:
installation-method: 'gem'
extra-flavors: 'iso bsi ribose'
github-packages-token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
```

### BSI Fontist Setup

When installing the BSI flavor, the action automatically configures fontist private formulas for proprietary fonts. This requires the `github-packages-token` to access the private fontist formulas repository.

### Migration from setup-flavors

If you were previously using the separate `setup-flavors` action:

**Before:**
```yaml
- uses: actions-mn/setup@v3
- uses: actions-mn/setup-flavors@v1
with:
extra-flavors: bsi
github-packages-token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
```

**After:**
```yaml
- uses: actions-mn/setup@v3
with:
installation-method: gem
extra-flavors: bsi
github-packages-token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
```

## Idempotent Installation

The action automatically detects when Metanorma is already installed with the same configuration and skips redundant installation steps. This is useful in workflows where the action may be called multiple times.
Expand Down
259 changes: 259 additions & 0 deletions __tests__/flavor-installer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
import {FlavorInstaller} from '../src/flavors/flavor-installer.js';
import type {IMetanormaSettings} from '../src/metanorma-settings.js';
import * as exec from '@actions/exec';

// Mock @actions/exec
vi.mock('@actions/exec', () => ({
exec: vi.fn()
}));

// Mock @actions/core
vi.mock('@actions/core', () => ({
info: vi.fn(),
debug: vi.fn(),
warning: vi.fn(),
error: vi.fn()
}));

// Mock Terminal
vi.mock('../src/terminal.js', () => ({
Terminal: class {
info = vi.fn();
success = vi.fn();
warning = vi.fn();
error = vi.fn();
debug = vi.fn();
}
}));

describe('FlavorInstaller', () => {
let mockExec: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.clearAllMocks();
mockExec = vi.mocked(exec.exec);
// Default mock - return 0 (success)
mockExec.mockResolvedValue(0);
});

afterEach(() => {
vi.clearAllMocks();
});

const createSettings = (
overrides: Partial<IMetanormaSettings> = {}
): IMetanormaSettings =>
({
version: null,
snapChannel: 'stable',
chocoPrerelease: false,
platform: 'linux',
installPath: '/tmp',
installationMethod: 'gem',
...overrides
}) as IMetanormaSettings;

describe('installFlavors', () => {
it('should do nothing when no flavors are configured', async () => {
const settings = createSettings({extraFlavors: []});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

expect(mockExec).not.toHaveBeenCalled();
});

it('should do nothing when extraFlavors is undefined', async () => {
const settings = createSettings();
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

expect(mockExec).not.toHaveBeenCalled();
});

it('should throw error when private flavors are requested without token', async () => {
const settings = createSettings({
extraFlavors: ['bsi'],
githubPackagesToken: undefined
});
const installer = new FlavorInstaller(settings);

await expect(installer.installFlavors()).rejects.toThrow(
'Private flavors (bsi) require github-packages-token to be set'
);
});

it('should configure GitHub Packages when token is provided', async () => {
const settings = createSettings({
extraFlavors: ['bsi'],
githubPackagesToken: 'test-token'
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining([
'config',
'https://rubygems.pkg.github.com/metanorma'
]),
expect.any(Object)
);
});

it('should install public flavor from RubyGems', async () => {
const settings = createSettings({
extraFlavors: ['iso']
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['add', 'metanorma-iso']),
expect.any(Object)
);
});

it('should install private flavor from GitHub Packages', async () => {
const settings = createSettings({
extraFlavors: ['bsi'],
githubPackagesToken: 'test-token'
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

// Should configure GitHub Packages
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['config']),
expect.any(Object)
);

// Should add private gem with source
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining([
'add',
'metanorma-bsi',
'--source',
'https://rubygems.pkg.github.com/metanorma'
]),
expect.any(Object)
);
});

it('should setup BSI fontist private formulas when BSI flavor is installed', async () => {
const settings = createSettings({
extraFlavors: ['bsi'],
githubPackagesToken: 'test-token'
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

// Should configure GitHub Packages
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['config']),
expect.any(Object)
);

// Should add private gem with source
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining([
'add',
'metanorma-bsi',
'--source',
'https://rubygems.pkg.github.com/metanorma'
]),
expect.any(Object)
);

// Should check fontist version
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['exec', 'fontist', '--version']),
expect.any(Object)
);

// Should run fontist update
expect(mockExec).toHaveBeenCalledWith(
'fontist',
expect.arrayContaining(['update']),
expect.any(Object)
);

// Should setup fontist repo
expect(mockExec).toHaveBeenCalledWith(
'fontist',
expect.arrayContaining(['repo', 'setup', 'metanorma']),
expect.any(Object)
);

// Should update fontist repo
expect(mockExec).toHaveBeenCalledWith(
'fontist',
expect.arrayContaining(['repo', 'update', 'metanorma']),
expect.any(Object)
);
});

it('should install multiple flavors', async () => {
const settings = createSettings({
extraFlavors: ['iso', 'ietf'],
githubPackagesToken: undefined
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

// Should install both public flavors
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['add', 'metanorma-iso']),
expect.any(Object)
);
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['add', 'metanorma-ietf']),
expect.any(Object)
);
});

it('should install mix of public and private flavors', async () => {
const settings = createSettings({
extraFlavors: ['iso', 'bsi'],
githubPackagesToken: 'test-token'
});
const installer = new FlavorInstaller(settings);

await installer.installFlavors();

// Should configure GitHub Packages for private flavor
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['config']),
expect.any(Object)
);

// Should install both flavors
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['add', 'metanorma-iso']),
expect.any(Object)
);
expect(mockExec).toHaveBeenCalledWith(
'bundle',
expect.arrayContaining(['add', 'metanorma-bsi']),
expect.any(Object)
);
});
});
});
Loading