Skip to content

PRO-8756: monorepo workflows - #5179

Merged
myovchev merged 59 commits into
mainfrom
pro-8756-monorepo-switch-workflows
Dec 4, 2025
Merged

myovchev merged 59 commits into
mainfrom
pro-8756-monorepo-switch-workflows

Conversation

@myovchev

@myovchev myovchev commented Nov 28, 2025 •

Copy link
Copy Markdown
Contributor

Summary

Brings a tooling for detecting the change impact and provide a matrix for performant testing in GitHub Actions (only test what makes sense).

Detection

  • Only packages that have test script in their package.json will participate
  • Automatically detecting what packages are affected by the current changes
  • If [force ci] (case insensitive) is found in a commit message, all packages will be tested
  • A nightly run will test all packages, everyday.
  • A manual dispatch is possible. If "Force tests for every package" is checked, all packages will be tested, otherwise only the auto detected packages will run.

Optimization & Configuration

The following configuration in any package/*/package.json`

  "apostropheTestConfig": {
    "requiresMongo": true,
    "requiresRedis": false
  }

will determine what services are needed during the CI test run for that package. The above showcases the default behavior if no configuration is found.

NODE_VERSIONS_JSON: "[20,22,24]" and MONGODB_VERSIONS_JSON: '["7","8"]' environment variables in monorepo.yml control the version matrix of the respective runtime. REDIS_VERSION: "7" defines the Redis version (when in use).

The services are pulled from the docker repository and cached for a month. No additional network requests will be made until cache miss.

Technical details

  • Retire the legacy top-level “Tests” workflow in favor of two new monorepo-aware pipelines: a PR/branch workflow that only runs packages touched by a change and a nightly schedule that fans out across every package.
  • Add .github/workflows/scripts/detect-impacted-packages.mjs, which diff-checks against the default branch, expands dependent packages, and feeds GitHub’s matrix jobs; mocha test ensures proper behavior. Example output.
  • Add .github/workflows/scripts/expand-runtime-matrix.mjs to merge the detected packages matrix with the nodejs and mongodb version matrix. Example output
  • Allow commit messages containing [force ci] to force testing of all monorepo packages that have test package JSON script.
  • Expose a run_all workflow-dispatch flag plus nightly FORCE_ALL runs so we can still exercise the full workspace on demand.

What are the specific steps to test this change?

  1. Install dependencies (pnpm install) so the repo scripts/tests are available.
  2. Run npx mocha .github/workflows/scripts/test-impacted-packages.mjs and confirm all detector tests pass.
  3. (Optional sanity check) Modify a file inside one package, run DEFAULT_BRANCH=main BASE_SHA=$(git merge-base HEAD origin/main) node .github/workflows/scripts/detect-impacted-packages.mjs, and verify the JSON output only lists that package and its dependents.

What kind of change does this PR introduce?

(Check at least one)

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Build-related changes
  • Other

Make sure the PR fulfills these requirements:

  • It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
  • The changelog is updated
  • Related documentation has been updated
  • Related tests have been updated

If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

Other information:

@linear

linear Bot commented Nov 28, 2025

Copy link
Copy Markdown

@myovchev
myovchev requested a review from ValJed November 28, 2025 15:31
@ValJed

ValJed commented Dec 1, 2025

Copy link
Copy Markdown
Contributor

The script will trigger test on all modules (that have test task) when a change in apostrophe is made
Ok this sounds good indeed.

I disagree that a module change can break the core. It can break the functionality brought by the module. Running apostrophe tests when module changes will NOT detect regression, the module test should detect it and as a final guard - e2e (Cypress). Also the setup includes a periodical nightly test run, the runs ALL package tests, once per day.

Yes it shouldn't but always skeptical, the nightly test run fixes the issue I think.

I'll review the core when I have a minute :)

another question: is there any tool handling this logic already?

Base automatically changed from pro-8756-monorepo-switch to main December 1, 2025 10:29
Comment thread .github/workflows/monorepo-nightly.yml Fixed
Comment thread .github/workflows/monorepo-nightly.yml Fixed
Comment thread .github/workflows/monorepo.yml Fixed
Comment thread .github/workflows/monorepo.yml Fixed
Comment thread packages/redirect/index.js Fixed
Comment thread packages/sanitize-html/index.js Fixed
Comment thread packages/uploadfs/test/s3.js Fixed
Comment thread .github/workflows/scripts/test-impacted-packages.mjs Fixed
});

function runGit(command) {
return execSync(`git ${command}`, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 10 months ago

To fix this issue, avoid concatenating user- or environment-derived values directly into a shell command string. Instead, pass the subcommand and arguments as an array to execFileSync (or execFile). This approach avoids shell interpretation and ensures that arguments with spaces or special characters are handled safely. Specifically, update runGit(command) to take either: (a) a command string and split it into args safely, or (b) a subcommand and arguments explicitly, or (c) accept an argument array. Since callers like gitAdd and gitCommit currently pass a command string, the cleanest safe fix is to update all these to build their argument arrays and pass them to runGit, which then invokes 'git' with those arguments using execFileSync (or execSync). Make sure to replace all invocations of runGit in the file accordingly.

Changes needed:

  • Update runGit to take an arguments array instead of a string.
  • Update all calls to runGit to pass argument arrays rather than strings.
  • Escape/safely handle commit message (git commit -m) and paths for add.
  • No new external dependencies are needed.

Suggested changeset 1
.github/workflows/scripts/test-impacted-packages.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/scripts/test-impacted-packages.mjs b/.github/workflows/scripts/test-impacted-packages.mjs
--- a/.github/workflows/scripts/test-impacted-packages.mjs
+++ b/.github/workflows/scripts/test-impacted-packages.mjs
@@ -142,8 +142,8 @@
   });
 });
 
-function runGit(command) {
-  return execSync(`git ${command}`, {
+function runGit(args) {
+  return execFileSync('git', args, {
     cwd: repoRoot,
     encoding: 'utf8',
     env: gitEnv,
@@ -154,15 +154,15 @@
 function gitAdd(filePath) {
   const relative = path.relative(repoRoot, filePath);
   const gitPath = relative.split(path.sep).join(path.posix.sep);
-  runGit(`add ${gitPath}`);
+  runGit(['add', gitPath]);
 }
 
 function gitCommit(message) {
-  runGit(`commit -m "${message}"`);
+  runGit(['commit', '-m', message]);
 }
 
 function ensureCleanWorkingTree() {
-  const status = runGit('status --porcelain');
+  const status = runGit(['status', '--porcelain']);
   if (status) {
     throw new Error('Working tree must be clean before running impact tests.');
   }
EOF
@@ -142,8 +142,8 @@
});
});

function runGit(command) {
return execSync(`git ${command}`, {
function runGit(args) {
return execFileSync('git', args, {
cwd: repoRoot,
encoding: 'utf8',
env: gitEnv,
@@ -154,15 +154,15 @@
function gitAdd(filePath) {
const relative = path.relative(repoRoot, filePath);
const gitPath = relative.split(path.sep).join(path.posix.sep);
runGit(`add ${gitPath}`);
runGit(['add', gitPath]);
}

function gitCommit(message) {
runGit(`commit -m "${message}"`);
runGit(['commit', '-m', message]);
}

function ensureCleanWorkingTree() {
const status = runGit('status --porcelain');
const status = runGit(['status', '--porcelain']);
if (status) {
throw new Error('Working tree must be clean before running impact tests.');
}
Copilot is powered by AI and may make mistakes. Always verify output.
@myovchev
myovchev requested a review from haroun December 3, 2025 12:06

@ValJed ValJed left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few questions, looks good!

}

const commitMessages = output.split('\n');
return commitMessages.some((msg) => /\[force ci\]/i.test(msg));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So any commit on your PR containing this force ci will make them all run all the time (for this PR).
Something in the title of the PR would have been easier to track but not sure we can do that. I think it's ok but I don't see use case especially with the daily run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a quick way to solve "not forget to run all tests for my change for some strange reason" . This follows the convention for github owned (skip ci)

}).trim();
return mergeBase || '';
} catch (error) {
return '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[detail] Could be null for clarity since we check if it exist later.

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pnpm version could be an env too maybe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a great future improvement, yes. But we can leave it for when we bump pnpm

with:
name: pnpm-lock
path: pnpm-lock.yaml
retention-days: 30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 30 days? Is is shared between CI runs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter, it's overridden on every run and used down the pipeline.


const GLOBAL_IMPACT_PATHS = new Set([
'package.json',
// Keep it, although it's not under version control - be explicit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but I see you use cache for the pnpm-lock file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generated on every run in the setup phase, reused in all other phases, including the test matrix jobs

},
"apostropheTestConfig": {
"requiresMongo": false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that everytime there are mocha tests we need mongo? (Not sure we might have unit tests).
If yes and if we follow a convention, any package without a script mocha could skip mongo. Your solution is clearer but I'm not sure people will think about updating this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think declaring explicitly what a package needs is a better way to go. For mongo it makes sense to explicitly opt-out, as most of the packages do require it.

@myovchev
myovchev merged commit c9aba85 into main Dec 4, 2025
218 checks passed
@myovchev
myovchev deleted the pro-8756-monorepo-switch-workflows branch December 4, 2025 10:46
haroun added a commit that referenced this pull request Dec 10, 2025
* main: (26 commits)
  more (#5207)
  adds permission.isAdmin (#5192)
  Skip string choices, disallow type change (#5203)
  Fix stylelint dependency conflict (#5199)
  oops old version number in main (#5201)
  just merging back the changelog (#5198)
  PRO-8758: soft-redirect and url encoding  (#5191)
  Fix admin UI crashes (#5195)
  PRO-8756: monorepo workflows (#5179)
  PRO-8768: relative paths for the good operating systems (#5194)
  bump express-cache-on-demand to 1.0.4 (#5190)
  the video is no longer available, use apostrophecms channel video wit… (#5183)
  add changelog for pro-8751 (#5187)
  pro 8751 schema update (#5176)
  Pro 8761 i18n static tests (#5185)
  Pro 8756 monorepo switch (#5177)
  release 4.24.0 (#5173)
  PRO-8743: windows fix 2b: back off on the file:/// URLs for vite (#5169)
  add batch failure notifications (#5157)
  PRO-8735 PR take 2 (#5168)
  ...
haroun added a commit that referenced this pull request Dec 10, 2025
* main: (26 commits)
  more (#5207)
  adds permission.isAdmin (#5192)
  Skip string choices, disallow type change (#5203)
  Fix stylelint dependency conflict (#5199)
  oops old version number in main (#5201)
  just merging back the changelog (#5198)
  PRO-8758: soft-redirect and url encoding  (#5191)
  Fix admin UI crashes (#5195)
  PRO-8756: monorepo workflows (#5179)
  PRO-8768: relative paths for the good operating systems (#5194)
  bump express-cache-on-demand to 1.0.4 (#5190)
  the video is no longer available, use apostrophecms channel video wit… (#5183)
  add changelog for pro-8751 (#5187)
  pro 8751 schema update (#5176)
  Pro 8761 i18n static tests (#5185)
  Pro 8756 monorepo switch (#5177)
  release 4.24.0 (#5173)
  PRO-8743: windows fix 2b: back off on the file:/// URLs for vite (#5169)
  add batch failure notifications (#5157)
  PRO-8735 PR take 2 (#5168)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants