Skip to content
Open
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
44 changes: 44 additions & 0 deletions .github/workflows/readme-automation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: readme-automation

'on':
workflow_dispatch:
push:
paths:
- "src/eoc.js"
concurrency:
group: readmeautomation-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04

steps:
- name: Check out repo
uses: actions/checkout@v6
- name: Use Node
uses: actions/setup-node@v6
with:
node-version: "18.x"
check-latest: false
- name: Clean install
run: npm ci
- name: Install global
run: npm install -g
- name: Run readme_automation script
run: |-
node scripts/readme_automation.js
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
commit-message: "chore(readme): insert sections into README from CLI help"
branch: chore/readme-automation
title: "chore(readme): insert sections into README from CLI help"
body: |
This PR was automatically generated by GitHub Actions.

- Insert sections into README from CLI help output
- Keeps documentation in sync with the CLI
60 changes: 24 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,42 +115,30 @@ That's it.
You can also do many other things with `eoc` commands
(the flow is explained in [this blog post][blog]):

* `register` finds necessary `.eo` files and registers them in a JSON catalog
* `assemble` parses `.eo` files into `.xmir`, optimizes them,
and pulls foreign EO objects
* `transpile` converts `.xmir` files to the target programming
language (Java by default)
* `compile` converts target language sources (e.g., `.java`)
to binaries (e.g., `.class`)
* `link` puts all binaries together into a single executable binary
* `dataize` dataizes a single object from the executable binary
* `test` dataizes all visible unit tests
* `lint` finds style-related errors in EO and XMIR files
* `jeo:disassemble` converts Java `.class` files to `.xmir`
(via [jeo](https://github.com/objectionary/jeo-maven-plugin))
* `jeo:assemble` converts `.xmir` files to Java `.class` files
(via [jeo](https://github.com/objectionary/jeo-maven-plugin))

There are also commands that help manipulate with XMIR and EO sources
(the list is not completed, while some of them are not implemented as of yet):

* `audit` inspects all required packages and reports their status
* `foreign` inspects all objects found in the program after the `assemble` step
* `sodg` generates `.sodg` from `.xmir`, further rederable as XML or [Dot][dot]
* `print` generates `.eo` files from `.xmir` files
* `generate_comments` generates `.json` files with LLM-generated
documentation for `.eo` structures
* `docs` generates HTML documentation from `.xmir` files
* `latex` generates `.tex` files from `.eo` sources
* `fmt` formats `.eo` files in the source directory
* `normalize` normalizes `.eo` files via phi-calculus rewriting using
[phino](https://github.com/objectionary/phino) (must be installed separately);
original files are saved to `.eoc/before-normalize/` for debugging
* ~~`translate` converts Java/C++/Python/etc. program to EO program~~
* ~~`demu` removes `cage` and `memory` objects~~
* ~~`dejump` removes `goto` objects~~
* ~~`infer` suggests object names where it's possible to infer them~~
* ~~`flatten` moves inner objects to upper level~~
<!-- BEGIN COMMANDS SECTION -->
* `audit` Inspect all packages and report their status
* `foreign` Inspect and print the list of foreign objects
* `clean` Delete all temporary files
* `register` Register all visible EO source files
* `parse` Parse EO files into XMIR
* `assemble` Parse EO files into XMIR and join them with required dependencies
* `sodg` Generate SODG files from XMIR
* `print` Generate EO files from XMIR files
* `lint` Lint XMIR files and fail if any issues inside
* `resolve` Resolve all the dependencies required for compilation
* `transpile` Convert EO files into target language
* `compile` Compile target language sources into binaries
* `link` Link together all binaries into a single executable binary
* `dataize` Run the single executable binary and dataize an object
* `test` Run all visible unit tests
* `docs` Generate documentation from XMIR files
* `generate_comments` Generate documentation with LLM
* `jeo:disassemble` Disassemble .class files to .xmir files
* `jeo:assemble` Assemble .xmir files to .class files
* `latex` Generate LaTeX files from EO sources
* `normalize` Normalize EO files using phi-calculus normalization via phino
* `fmt` Format EO files in the source directory
<!-- END COMMANDS SECTION -->

This command line toolkit simply integrates other tools available in
the [@objectionary](https://github.com/objectionary) GitHub organization.
Expand Down
50 changes: 50 additions & 0 deletions scripts/readme_automation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env node
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/

const assert = require('assert');
const { commandsDescription } = require("../src/eoc");

function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function updateSection(sectionName, newContent, readMeContent) {
const start = `<!-- BEGIN ${sectionName.toUpperCase()} SECTION -->`;
const end = `<!-- END ${sectionName.toUpperCase()} SECTION -->`;
const regex = new RegExp(
`(${escapeRegex(start)})([\\s\\S]*?)(${escapeRegex(end)})`,
"g"
);
return readMeContent.replace(regex, `$1\n${newContent}$3`);
}

function bulletListTemplate(rows) {
return `${rows.map(([cmd, desc]) => `* \`${cmd}\` ${desc}`).join("\n") }\n`;
}

function modifyReadme(commands,fs) {
assert.ok(commands.length > 0,'Commands should have rows')
const commandsMarkdown = bulletListTemplate(commands);
assert.ok(commandsMarkdown.length > 0,"commandsMarkdown result should have text")
let readMeContent = fs.readFileSync('README.md', "utf8");
assert.ok(readMeContent.length > 0,"readMeContent should have text")
readMeContent = updateSection('commands', commandsMarkdown, readMeContent);
assert.ok(readMeContent.length > 0,"readMeContent should sill have text after modification")
fs.writeFileSync('README.md',readMeContent);
}

function syncCommandsToReadme(fs)
{
modifyReadme(commandsDescription(),fs);
}

module.exports = {
bulletListTemplate, updateSection, modifyReadme, syncCommandsToReadme
};

if (require.main === module) {
syncCommandsToReadme(require("fs"));
};
19 changes: 13 additions & 6 deletions src/eoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,19 @@ program.command('fmt')
});
});

try {
program.parse(process.argv);
} catch (e) {
console.error(e.message);
console.debug(e.stack);
process.exit(1);
if (require.main === module) {
try {
program.parse(process.argv);
} catch (e) {
console.error(e.message);
console.debug(e.stack);
process.exit(1);
}
}

module.exports.commandsDescription = function commandsDescription() {
return program.commands
.map(c => [c.name(),c.description()]);
}

/**
Expand Down
8 changes: 8 additions & 0 deletions test/test_eoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ describe('eoc', () => {
assert(stdout.includes(version.when));
done();
});
it('can get commands description from eoc as a module', (done) => {
const commandsDescriptionList = require('../src/eoc').commandsDescription();
assert(commandsDescriptionList.length > 0,"commandsDescriptionList should have more then one element");
assert(commandsDescriptionList[0].length == 2,"commandsDescriptionList element should have 2 values");
assert(commandsDescriptionList[0][0].length > 0,"First value of commandsDescriptionList element should have non-zero length");
assert(commandsDescriptionList[0][1].length > 0,"Second value of commandsDescriptionList element should have non-zero length");
done();
});
});

describe('eoc', () => {
Expand Down
40 changes: 40 additions & 0 deletions test/test_readme_automation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/

const assert = require("assert");
const path = require("path");
const { updateSection, bulletListTemplate, modifyReadme, syncCommandsToReadme} = require(path.join(__dirname, "../scripts/readme_automation.js"));

const readme = `before\n<!-- BEGIN COMMANDS SECTION -->\nold content\n<!-- END COMMANDS SECTION -->\n<!-- BEGIN OPTIONS SECTION -->\nkeep\n<!-- END OPTIONS SECTION -->\nafter`
const commandsList = [['one','a'],['two','b'],['three','c']];
const keywords = ["keep","after","before","<!-- BEGIN COMMANDS SECTION -->","<!-- END COMMANDS SECTION -->"];
const fsMockProto = {readMeContent:null, readFileSync(path,options){ return readme; }, writeFileSync(path,data){ this.readMeContent = data; }};


describe("readme_automation scripts", () => {
it("updateSection replaces only the requested section", () => {
const readmeUpdated = updateSection("commands", "new content", readme);
assert.ok(!readmeUpdated.includes("old content"),"Update README should no have 'old content'");
assert.ok(keywords.every(sub => readmeUpdated.includes(sub)),"Update README should have all keywords");
assert.ok(readmeUpdated.includes("new content"),"Update README should have `new content`");
});
it("bulletListTemplate renders a list", async () => {
const bulletListMarkdown = bulletListTemplate(commandsList);
assert.strictEqual(bulletListMarkdown, "* `one` a\n* `two` b\n* `three` c\n");
});
it("call modifyReadme with fs mock", async () => {
const fsMock = {...fsMockProto};
modifyReadme(commandsList,fsMock);
assert.notEqual(fsMock.readMeContent,null,"README file should have some content");
assert.ok(!fsMock.readMeContent.includes("old content"),"README file should no have 'old content'");
assert.ok(keywords.every(sub => fsMock.readMeContent.includes(sub)),"README file should have all keywords");
assert.ok(["* `one` a","* `two` b","* `three` c"].every(sub => fsMock.readMeContent.includes(sub)));
});
it("test the whole workflow with fs mock", async () => {
const fsMock = {...fsMockProto};
syncCommandsToReadme(fsMock);
assert.notEqual(fsMock.readMeContent,null,"README file should have some content");
});
});