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
10 changes: 10 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,13 @@ pnpm build
```

The GitHub actions pipeline will attempt to build the actions and check if there are any differences between the built files and those that are committed to the repository. If there are any differences, the pipeline will fail.

## Testing

Tests run on Node's built-in test runner, which strips the TypeScript types itself, so no build step is needed. To run every test in the workspace:

```bash
pnpm test
```

Tests live next to the code they cover, as `*.test.ts`.
23 changes: 23 additions & 0 deletions .github/workflows/self-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
merge_group:
pull_request:

concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

jobs:
test:
name: test:required
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PNPM
uses: ./actions/setup-pnpm

- name: Run tests
run: pnpm run test
1 change: 1 addition & 0 deletions actions/assemble-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This action:

- Zips the `assets_dir` directory into `{target_dir}/{version}.zip`
- Upserts the entry to `versions.json` for the `version`
- Reorders `versions.json` with `latest` first, followed by the remaining versions in descending numeric order (e.g. `v10.0` before `v9.0`)

## Action inputs

Expand Down
57 changes: 44 additions & 13 deletions actions/assemble-docs/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21336,21 +21336,60 @@ var import_action_utils = __toESM(require_dist());
var LATEST_VERSION_NAME = "latest";
var VALID_TAGS = ["latest", "beta", "dev", "next", "nightly", "canary"];
var VALID_VERSION_FORMATS = ["vX", "vX.Y", "vX.Y.Z", ...VALID_TAGS];
var VALID_VERSION_PATTERNS = new RegExp(
`^(?:v\\d+(?:\\.\\d+(?:\\.\\d+)?)?|${VALID_TAGS.join("|")})$`
var VERSION_PART_COUNT = 3;
var NUMERIC_VERSION_SOURCE = `v\\d+(?:\\.\\d+){0,${VERSION_PART_COUNT - 1}}`;
var NUMERIC_VERSION_PATTERN = new RegExp(`^${NUMERIC_VERSION_SOURCE}$`);
var VALID_VERSION_PATTERN = new RegExp(
`^(?:${NUMERIC_VERSION_SOURCE}|${VALID_TAGS.join("|")})$`
);
var ALLOWED_VERSIONS_MESSAGE = `Allowed values: ${VALID_VERSION_FORMATS.join(" | ")}`;
function isVersionListedInVersionsJson(version) {
return version.startsWith("v") || version === LATEST_VERSION_NAME;
}
function isValidVersion(version) {
return VALID_VERSION_PATTERNS.test(version);
return VALID_VERSION_PATTERN.test(version);
}
function parseVersionPath(versionPath) {
if (!NUMERIC_VERSION_PATTERN.test(versionPath)) {
return null;
}
const parts = versionPath.slice(1).split(".").map(Number);
const omittedParts = Array(VERSION_PART_COUNT - parts.length).fill(0);
return [...parts, ...omittedParts];
}
function compareVersionPaths(a, b) {
if (a === b) {
return 0;
}
if (a === LATEST_VERSION_NAME) {
return -1;
}
if (b === LATEST_VERSION_NAME) {
return 1;
}
const partsA = parseVersionPath(a);
const partsB = parseVersionPath(b);
if (partsA === null || partsB === null) {
if (partsA !== null) {
return -1;
}
if (partsB !== null) {
return 1;
}
return a.localeCompare(b);
}
for (let i = 0; i < VERSION_PART_COUNT; i++) {
if (partsA[i] !== partsB[i]) {
return partsB[i] - partsA[i];
}
}
return a.localeCompare(b);
}

// src/upsert-versions-json.ts
async function upsertVersionsJson(params) {
const { versionsJsonPath, version, versionLabel, versionInTitle } = params;
let versions = (0, import_action_utils.readJsonFile)(versionsJsonPath) || [];
const versions = (0, import_action_utils.readJsonFile)(versionsJsonPath) || [];
const versionEntryIndex = versions.findIndex((v) => v.path === version);
if (versionEntryIndex !== -1) {
const versionEntry = versions[versionEntryIndex];
Expand All @@ -21371,15 +21410,7 @@ async function upsertVersionsJson(params) {
}
versions.push(newVersionEntry);
}
versions = versions.sort((a, b) => {
if (a.path === LATEST_VERSION_NAME && b.path !== LATEST_VERSION_NAME) {
return -1;
}
if (b.path === LATEST_VERSION_NAME && a.path !== LATEST_VERSION_NAME) {
return 1;
}
return b.path.localeCompare(a.path);
});
versions.sort((a, b) => compareVersionPaths(a.path, b.path));
(0, import_action_utils.writeJsonFile)(versionsJsonPath, versions);
}

Expand Down
3 changes: 2 additions & 1 deletion actions/assemble-docs/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "@dfinity/assemble-docs-action",
"scripts": {
"build": "tsc && node ./build.mjs"
"build": "tsc && node ./build.mjs",
"test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test 'src/**/*.test.ts'"
},
"dependencies": {
"@dfinity/action-utils": "workspace:*"
Expand Down
15 changes: 3 additions & 12 deletions actions/assemble-docs/src/upsert-versions-json.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { writeJsonFile, readJsonFile } from '@dfinity/action-utils';
import { LATEST_VERSION_NAME } from './versions';
import { compareVersionPaths } from './versions';

type VersionEntry = { path: string; label: string; versionInTitle?: string };

Expand All @@ -11,7 +11,7 @@ export async function upsertVersionsJson(params: {
}): Promise<void> {
const { versionsJsonPath, version, versionLabel, versionInTitle } = params;

let versions = readJsonFile<VersionEntry[]>(versionsJsonPath) || [];
const versions = readJsonFile<VersionEntry[]>(versionsJsonPath) || [];

const versionEntryIndex = versions.findIndex(v => v.path === version);
if (versionEntryIndex !== -1) {
Expand All @@ -34,16 +34,7 @@ export async function upsertVersionsJson(params: {
versions.push(newVersionEntry);
}

// Sort versions: latest first, then reverse alphabetically by path
versions = versions.sort((a, b) => {
if (a.path === LATEST_VERSION_NAME && b.path !== LATEST_VERSION_NAME) {
return -1;
}
if (b.path === LATEST_VERSION_NAME && a.path !== LATEST_VERSION_NAME) {
return 1;
}
return b.path.localeCompare(a.path);
});
versions.sort((a, b) => compareVersionPaths(a.path, b.path));

writeJsonFile(versionsJsonPath, versions);
}
185 changes: 185 additions & 0 deletions actions/assemble-docs/src/versions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
ALLOWED_VERSIONS_MESSAGE,
compareVersionPaths,
isValidVersion,
isVersionListedInVersionsJson,
} from './versions.ts';

function sorted(paths: string[]): string[] {
return [...paths].sort(compareVersionPaths);
}

describe('isValidVersion', () => {
for (const version of ['v1', 'v1.2', 'v1.2.3', 'v10.0', 'v100.200.300']) {
it(`accepts ${version}`, () => {
assert.equal(isValidVersion(version), true);
});
}

for (const tag of ['latest', 'beta', 'dev', 'next', 'nightly', 'canary']) {
it(`accepts the ${tag} tag`, () => {
assert.equal(isValidVersion(tag), true);
});
}

for (const version of [
'',
'1.2.3',
'v',
'v1.2.3.4',
'v1.',
'va',
'v1-beta',
'V1.2.3',
'stable',
]) {
it(`rejects ${JSON.stringify(version)}`, () => {
assert.equal(isValidVersion(version), false);
});
}

it('lists every accepted format in the error message', () => {
assert.equal(
ALLOWED_VERSIONS_MESSAGE,
'Allowed values: vX | vX.Y | vX.Y.Z | latest | beta | dev | next | nightly | canary',
);
});
});

describe('isVersionListedInVersionsJson', () => {
it('lists numeric versions and latest', () => {
assert.equal(isVersionListedInVersionsJson('v10.0'), true);
assert.equal(isVersionListedInVersionsJson('latest'), true);
});

it('omits the other tags', () => {
assert.equal(isVersionListedInVersionsJson('beta'), false);
assert.equal(isVersionListedInVersionsJson('nightly'), false);
});
});

describe('compareVersionPaths', () => {
it('orders a two-digit major above a one-digit major', () => {
assert.deepEqual(sorted(['v9.0', 'v10.0']), ['v10.0', 'v9.0']);
});

it('keeps latest first', () => {
assert.deepEqual(sorted(['v4.0', 'latest', 'v10.0']), [
'latest',
'v10.0',
'v4.0',
]);
});

it('orders the auth versions as the dropdown shows them', () => {
assert.deepEqual(
sorted([
'latest',
'v9.0',
'v8.0',
'v7.1',
'v7.0',
'v6.2',
'v6.1',
'v6.0',
'v5.0',
'v4.2',
'v4.1',
'v4.0',
'v10.0',
]),
[
'latest',
'v10.0',
'v9.0',
'v8.0',
'v7.1',
'v7.0',
'v6.2',
'v6.1',
'v6.0',
'v5.0',
'v4.2',
'v4.1',
'v4.0',
],
);
});

it('compares each part numerically', () => {
assert.deepEqual(sorted(['v1.9.9', 'v1.10.0', 'v1.10.1', 'v2.0.0']), [
'v2.0.0',
'v1.10.1',
'v1.10.0',
'v1.9.9',
]);
});

it('orders a two-digit minor above a one-digit minor', () => {
assert.deepEqual(sorted(['v0.9', 'v0.23', 'v0.13', 'v0.2']), [
'v0.23',
'v0.13',
'v0.9',
'v0.2',
]);
});

it('treats omitted parts as zero', () => {
assert.deepEqual(sorted(['v3.1', 'v3', 'v3.0.1']), [
'v3.1',
'v3.0.1',
'v3',
]);
});

it('orders paths for the same version deterministically', () => {
assert.deepEqual(sorted(['v3.0.0', 'v3', 'v3.0']), [
'v3',
'v3.0',
'v3.0.0',
]);
});

it('places non-numeric paths last, alphabetically', () => {
assert.deepEqual(sorted(['next', 'v1.0', 'beta', 'latest', 'v2.0']), [
'latest',
'v2.0',
'v1.0',
'beta',
'next',
]);
});

it('is a consistent ordering', () => {
const paths = [
'latest',
'v100.0',
'v10.1',
'v10.0.1',
'v10',
'v9.0',
'v2.0',
'v0.10',
'v0.9',
'beta',
];
const expected = sorted(paths);

assert.deepEqual(sorted([...paths].reverse()), expected);

for (const a of paths) {
assert.equal(compareVersionPaths(a, a), 0, `${a} equals itself`);

for (const b of paths) {
assert.equal(
Math.sign(compareVersionPaths(a, b)) +
Math.sign(compareVersionPaths(b, a)),
0,
`${a} and ${b} compare symmetrically`,
);
}
}
});
});
Loading
Loading