chore(release): promote staging to main for v14.0.27#216
Merged
Conversation
Merge code
β¦phase1 π§ fix: Phase 1 - Standardize naming conventions (16 HIGH severity issues)
`verifyJWTTokenWithC` and `verifyJWTTokenWithCV2` were passing
`new RegExp(companyId)` β where `companyId` comes straight from the
`companyid` request header β as the `audience` option to `jwt.verify`.
A header value like `.*` matched any company in the token audience,
allowing cross-tenant access; the same input doubled as a ReDoS vector.
Replace the regex with two explicit checks:
1. Reject any `companyid` that isn't a 24-char Mongo ObjectId
(defense-in-depth against control chars / regex metacharacters).
2. Verify the JWT without an `audience` option, then run a strict
membership check (`isCompanyInAudience`) against the comma-joined
`aud` claim β exact match per entry, trim-tolerant.
Response shapes and HTTP codes are unchanged, so existing clients see
no behavioural diff on legitimate requests. Cross-tenant headers now
return 401 instead of leaking data.
Closes #55
β¦002 / #56) `app.use(cors({origin: '*'}))` let every browser origin call every API route β combined with credentialed cookies and JS-readable tokens this opened a CSRF / token-theft path from any third-party origin. Introduce `utils/cors.js` with three helpers: - `buildCorsAllowList(env)` β derives the allow-list from WEBURL, APIURL, and an optional CORS_ORIGINS comma-list. - `isOriginAllowed(origin, list)` β exact-match check, trim/trailing- slash tolerant, and permissive for no-Origin / `null` / `file://` cases so curl, native mobile clients, and the Electron desktop build keep working. - `corsOriginDelegate` β drop-in `origin:` callback for the `cors` middleware. `index.js` now wires that delegate into `app.use(cors({...}))`, and `.env.example` documents the new optional `CORS_ORIGINS` variable for multi-domain deployments. The helper is intentionally generic so the Socket.io fix (BUG-003) can reuse it. Closes #56
β¦UG-003 / #57) The Socket.io server was instantiated with `cors: {origin: '*', credentials: true}`, the configuration browsers reject when sane β but engine.io accepts it. Any malicious origin could open an authenticated websocket on behalf of a logged-in victim, receive their realtime task/comment stream, and emit events as them. Reuse the shared CORS allow-list helper at `utils/cors.js` (same module introduced for the Express HTTP fix in BUG-002 / #56) and wire it into Socket.io's `cors.origin` callback. `credentials: true` is kept β it is safe now that origins are explicitly checked. `utils/cors.js` and the `CORS_ORIGINS` entry in `.env.example` are duplicated with the BUG-002 PR (#103) on purpose: each fix branches from `staging` independently per the rollout plan, and git merges identical blobs as a no-op so order of landing does not matter. Closes #57
β¦-004 / #58) The Socket.io admin UI was instantiated with hardcoded credentials: instrument(io, { auth: { type: "basic", username: "alian", password: "$2a$12$HHe..." }, namespaceName: "/admin", mode: "development" }); Anyone with read access to the repo could: - reuse the bcrypt hash, or compute the cleartext offline, - reach the deployed `/admin` namespace, - read connected sockets / rooms / events and emit events as any user. `mode: "development"` further disabled the TLS-only enforcement on the admin namespace in production deployments. Replace with a small env-driven helper `getAdminUiConfig(env)`: - Requires BOTH `SOCKETIO_ADMIN_USERNAME` and `SOCKETIO_ADMIN_PASSWORD_HASH` to be set (non-empty, non-whitespace). If either is missing the helper returns null and `instrument()` is skipped β the admin UI is disabled entirely. This is the new default for fresh installs and for any deployment that doesn't explicitly opt in. - `mode` follows `NODE_ENV`: `production` enables TLS-only protection on the admin namespace; everything else (including unrecognised values) stays `development` to fail closed. `socket/socketinit.js` exports the helper so the regression suite at `.claude/tests/test-bug-004.js` can exercise it directly. `.env.example` documents the new variables and how to generate a bcrypt hash. Closes #58
β¦(BUG-005 / #59) The `/api/v2/sendForgotPasswordEmail` flow built reset tokens as let temp = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let token = ''; for (let i = 0; i < 8; i++) { token += temp.charAt(Math.floor(Math.random() * temp.length)); } β 8 chars from a 62-char alphabet (~48 bits) drawn from `Math.random()`, which is neither cryptographically secure nor large enough to resist brute force against the reset endpoint. Replace with a single helper: exports.generateResetToken = () => crypto.randomBytes(32).toString('hex'); That's 32 bytes (256 bits) of CSPRNG output, hex-encoded to 64 URL-safe characters. The reset link format, DB shape, and call sites are unchanged β only the token-generation expression is swapped. The other forgot-password route (`/api/v2/auth/forgot-password` β `exports.sendForgotPassword`) already used a JWT and is unaffected. Closes #59
β¦07 / #61) `GET /api/v1/checkAvaibility` was registered in `Modules/auth/routes.js:9` and handled by `exports.checkAvaibility` in `Modules/auth/controller.js:109`. The handler did a `fs.readFileSync('./utils/licensesValidate.js')` and returned the file's source in the JSON body β with no authentication and no rate limit. The file does not even exist in the repository, and `git grep` shows zero callers under `Modules/`, `utils/`, or `frontend/src/`, so this is pure dead code that doubled as a source-disclosure / LFI attack surface (today returns ENOENT; on any deployment that adds the file it would dump licensing logic verbatim). Delete the route, the controller function, and the now-unused `const fs = require("fs")` import. No backward-compat concerns. Closes #61
β¦ (BUG-009 / #63) `Modules/createProject/controller.js:104` had `reject(error)` in the outer `catch` block of `createProjectFun`. The function is declared as `async (req, res) => { ... }`, not a `new Promise` constructor body, so `reject` was never defined in that scope. Any synchronous throw inside the surrounding `try { β¦ }` (most easily triggered by a malformed request body that breaks `checkProjectPlan`'s synchronous prelude) hit that catch, threw a `ReferenceError`, masked the original error, and left the response hanging until the client or upstream proxy timed out β usually 30β60s. Replace `reject(error)` with the same response shape the rest of the function already uses, and log the underlying error via Winston so the real cause is captured: logger.error(`createProjectFun error: ${error.message || error}`); res.status(400).send({ status: false, statusText: error.message || error, }); Out of scope here: BUG-010 (#64) β the `if (data.status) { β¦ }` branch without an `else` that causes the same handler to hang when `checkProjectPlan` resolves with `{status: false}`. Tracked separately. Closes #63
`Modules/createProject/controller.js:83-93` had an `if (data.status) { β¦ }`
on the resolution of `exports.checkProjectPlan(req)` with no `else`
branch. Today every failure path of `checkProjectPlan` happens to go
through `reject`, so the missing branch isn't currently reachable β but
the code is one refactor away from a silent hang:
- If anyone ever moves a failure path from `reject({status:false})`
to `resolve({status:false})` (a common Promise refactor), the
handler falls through with no response and the request hangs until
the upstream proxy times out.
- `checkProjectPlan` increments `projectCount.projectCount` (and one
of `publicCount`/`privateCount`) on the company document *before*
it validates plan limits. Any future failure path that lands here
via `resolve` would leave that speculative increment in place.
Add a defensive `else` branch that mirrors the existing `.catch`
rollback logic:
} else {
exports.removeProjectCount(req.body.CompanyId, req.body.isPrivateSpace);
res.status(400).send({
status: false,
statusText: (data && data.statusText) || 'Project plan check did not pass.',
});
}
The branch is reachable today only via test-time stubs (the regression
test exercises it) but closes the latent hang for any future change to
the validation contract.
Sibling fix BUG-009 (#63) β `reject(error)` in the outer `catch` β is
its own PR.
Closes #64
β¦ce-bypass fix(security): reject regex audience in JWT verify (BUG-001 / #55)
β¦list fix(security): replace wildcard CORS with env-driven allow-list (BUG-002 / #56)
fix(security): replace wildcard Socket.io CORS with env allow-list (BUG-003 / #57)
β¦dmin-creds fix(security): move Socket.io admin UI creds to env, default-off (BUG-004 / #58)
β¦n-entropy fix(security): generate password-reset token with crypto.randomBytes (BUG-005 / #59)
β¦ckavaibility fix(security): remove unauthenticated file-disclosure endpoint (BUG-007 / #61)
β¦side-promise fix(stability): respond instead of ReferenceError in createProjectFun (BUG-009 / #63)
β¦ect-missing-else fix(stability): add missing else in createProjectFun (BUG-010 / #64)
β¦-011 / #65) `Modules/auth/controller/verifyInvitation.js:26-42` decoded the base64 invitation blob from `req.body.id`, parsed it `&`/`=`-style, and then: req.body = finalObj; That replaced `req.body` with whatever key/value pairs the attacker chose to encode β no allow-list, no shape validation, and any later code reading `req.body.<anything>` would see attacker input as if it had been validated. Combined with predictable invitation tokens (the broader BUG-005 / #59 family) it was a parameter-injection primitive. Introduce a small exported pure helper: exports.parseInviteBlob = (encoded) => { β¦ } It accepts only the keys actually consumed downstream β `userId`, `companyId`, `linkId`, `docId` β and returns either a fresh local object containing just those keys or `null` for invalid input. It also: - Validates the input alphabet with a strict base64 regex before invoking `atob` (the npm `atob@2.1.2` package is permissive and happily returns garbage on malformed input). - Splits `key=value` on the FIRST `=` only, so values that legally contain `=` aren't truncated. - Skips parts without `=`. - Returns `null` if the blob contains none of the allow-listed keys. `exports.checkPermission` now reads from a local `invite` object returned by `parseInviteBlob` and `req.body` is left untouched. Closes #65
`Modules/auth/helper.js:180-194` hard-coded the auth rate-limit numbers inside `manageResetAttempt`: - 9 failed attempts allowed inside a window, - window labelled `fiveMinutes` but actually 10 min, - 10-minute block once tripped. That left every login / forgot-password / reset-password endpoint at roughly 9 attempts per IP per 10 minutes β permissive enough that a patient distributed-credential-stuffing attacker could grind through indefinitely. The variable name `fiveMinutes` also disagreed with its value, which is a maintenance trap. Tighten the defaults and lift the knobs out to env: AUTH_RATE_LIMIT_MAX_ATTEMPTS default 5 (was 9) AUTH_RATE_LIMIT_WINDOW_MS default 15 min AUTH_RATE_LIMIT_BLOCK_MS default 30 min (was 10) A new exported helper `getRateLimitConfig()` reads from `process.env`, falls back to safe defaults on missing / non-numeric / zero input, and clamps to sane minimums (>=1 attempt, >=1s window/block). It's read at request time so an operator can adjust without restarting. `.env.example` documents the three new variables; operators who need laxer behaviour for staging / load testing can raise them. The bad `fiveMinutes` variable name is gone; the threshold check now uses `attempts >= MAX_ATTEMPTS` so the configured limit means what it says. Closes #66
β¦G-013 / #67) The JWT audience claim is frozen at login and lasts JWT_EXP (24h default). A user removed from a company between login and token expiry kept access until the token expired β for up to a day on defaults. Add a live membership re-check that runs after the existing audience verification inside `verifyJWTTokenWithC` and `verifyJWTTokenWithCV2`: verifyCompanyMembership(uid, companyId) 1. Validate uid/companyId look like ObjectIds (cheap rejection of garbage so we don't waste a Mongo round-trip). 2. Check the in-memory cache (`node-cache`) β separate positive and negative entries. 3. Fall back to `users.findOne({_id, AssignCompany})` and cache the boolean for `MEMBERSHIP_CACHE_TTL_SECONDS` (default 60s). invalidateMembershipCache(uid, companyId?) - Exported so the membership-change flow (add / remove member) can wipe the cache and surface changes immediately. When the check fails, the middleware now returns HTTP 403 with `isLogout: true` so the frontend logs the user out rather than silently retrying with a token that the server no longer trusts. Both middlewares are now `async`. The membership cost is one Mongo lookup per (user, company) per minute by default β negligible next to the existing JWT verify on the same path. Doc: `MEMBERSHIP_CACHE_TTL_SECONDS` added to `.env.example` so operators can dial it down (faster removal propagation) or up (less Mongo traffic). Closes #67
β¦-014 / #68) `Modules/auth/controller/verifyEmail.js` had three bugs that combined into an exploitable verification bypass: 1. Input validation used `!req.body.token`, which lets falsy non-empty non-strings through. `![]` is `false`, so `{ token: [] }` passed. 2. The success branch used `==` (loose equality): response.verificationToken == req.body.token `"" == []` is `true` under JavaScript's loose-equality rules. The codebase stores `verificationToken: ""` both after a successful verification and as a fresh-account default, so any account in that state could be re-verified by sending `{ uid, token: [] }`. 3. There was no fallback `else`, so unmatched states (e.g. stored token was `null` or `undefined`) silently fell off the end and left the response hanging until the proxy timed out. Also: the expiry check called new Date(verificationTokenTime).setMinutes(...) on potentially-missing fields. `new Date(null)` is the epoch and `new Date(undefined)` is `Invalid Date`. Either way the resulting `ValidTime < new Date()` comparison silently bypassed the expiry for documents missing `verificationTokenTime`. The rewrite: - Validates `uid` and `token` with `typeof x === 'string' && x.length` so arrays / objects / numbers / null are rejected up front. - Validates `response.verificationToken` is a non-empty string before comparing β empty / null / undefined stored token β "expired". - Treats missing or invalid `verificationTokenTime` as expired rather than silently bypassing the 10-minute window. - Uses strict equality `===` for the token comparison. - Sends exactly one response in every branch so the request never hangs. Closes #68
β¦016 / #70) `Modules/tasks/helpers/mongo_helper.js` had two sites where `.catch` handlers called `reject(error)` AFTER the outer Promise had already been settled by an earlier `resolve(...)`. Calling `reject` on an already-settled Promise is a no-op, so the underlying history / inner-update failures were silently swallowed. Site 1 (the explicit bug-report site, lines 80-99 of `HandleTask`): MongoDbCrudOpration(...).then((response) => { socketEmitter.emit(...); resolve({status: true, ...}); // β settles here exports.HandleHistory('task', ...) .catch((error) => { reject(error); }); // β no-op, log lost exports.HandleHistory('project', ...) .catch((error) => { reject(error); }); // β no-op, log lost }) Refactor to: MongoDbCrudOpration(...).then(async (response) => { socketEmitter.emit(...); await Promise.allSettled([ exports.HandleHistory('task', ...).catch(log), exports.HandleHistory('project', ...).catch(log), ]); resolve({status: true, ...}); }) `Promise.allSettled` makes a single history failure non-fatal (the batch continues) and each per-task `.catch` logs via `logger.error` so the failure is now visible. Site 2 (the sibling instance at `convertToSubTaskFunction:335-337`): MongoDbCrudOpration(...).then((result) => { ...lots of code, including resolve(...)... }).catch((error) => { reject(error); }) // β also no-op The inner `.catch` here is reachable only after the inner `resolve(...)` has already settled the outer Promise (Mongo settles the inner before the .catch fires, and resolve happens inside the .then). Replace `reject(error)` with `logger.error(...)` so the failure is logged instead of silently dropped. Closes #70
β¦17 / #71) `array.forEach(async x => { β¦ })` doesn't wait for the callbacks and silently swallows any rejection. The audit flagged ~14 sites across the codebase. After reading each in context they split into two categories: CATEGORY B β real concurrency bug (the callback actually had an `await` inside, so the loop "completed" before the awaited work did): - Modules/AI/controller.js:387 β `await limitCountUpdate(...)` was fire-and-forget, so per-chunk quota updates raced the outer resolve. Replace with `forβ¦of` + `await` (serial; at most one chunk has `usage` per response so serial is the natural shape). - Modules/storage/server/helpers/bucket.helper.js:195 β `await copyFile(...)` was fire-and-forget, so `resolve()` ran before any file had finished copying. Replace with `await Promise.allSettled(imageArray.map(async ...))` so copies run in parallel and a single failure doesn't abort the batch. Also fix a latent secondary bug uncovered during the test: `copyFile` itself did `await fs.cp(source, dest, callback)`. `fs.cp` is callback-style and returns `undefined`, so `await` resolved immediately and the function returned before the copy started β the surrounding `Promise.allSettled` fix would have been toothless without this. Promisify the callback so awaits up the chain actually wait. CATEGORY A β the `async` keyword was unused (no `await` inside, callback was sync). The keyword was misleading but harmless. Drop it so the pattern is consistent everywhere: - Modules/AI/controller.js:395 (string concat) - Modules/projectSetting/controller.js:57, 154, 158 (uses .then chain) - frontend/src/store/ProjectData/actions.js:206, 261 (Vuex commit) - frontend/src/components/molecules/TaskAudioFiles/TaskAudioFiles.vue :351, :419, :424 (object construction) - frontend/src/components/organisms/HourlyMilestone/helper.js:194 - frontend/src/components/templates/CreateProject/TaskStatusForm.vue :317 - frontend/src/components/templates/CreateProject/ProjectTaskTypeForm.vue :266 Frontend build verified clean (`npm run build`). No remaining `forEach(async ...)` patterns in executable code (only in fix-comments that reference the old pattern). Closes #71
The audit description for BUG-018 β `Promise.allSettled rejection
branch never responds, outer Promise stays pending` β turned out to be
inaccurate in two ways once verified against the source:
1. The `else` branch on `if (rejected.length === 0)` already existed
and called `reject({status: false, statusText: 'error in
createProject'})`. The outer Promise did NOT hang.
2. `Promise.allSettled` never rejects in the first place β it always
resolves with the per-promise outcomes. So even without an else,
the surrounding `.then` would still fire.
What WAS broken (and is fixed here) is the loss of debugging info: the
existing else body was a generic statusText, with every per-query
`reason` in the `rejected` array discarded. The caller and the log
learnt nothing about WHICH prerequisite query failed or WHY.
Replace the else's reject body with a structured payload that includes:
- `rejectedCount` / `totalCount`
- `reasons`: array of `{index, reason}` carrying the actual error
message for each failed prerequisite query.
β¦and log the same array via `logger.error` so the failure is also in
the server log for ops.
Closes #72
`Modules/MainChats/controller.js:71` did:
setChat = await MongoDbCrudOpration(req.headers['companyid'], β¦);
with no `const`/`let`/`var`. That makes `setChat` an implicit global on
the Node process. Two concurrent requests share the same slot and can
clobber each other β the second response can include the first
request's data. (Under strict mode the assignment throws
`ReferenceError`; the file isn't strict today, so the live behaviour is
the silent cross-request leak.)
Trivial fix: add `const`. The variable is only used on the very next
line, so block scope is the right shape.
Closes #73
`Modules/MainChats/controller.js` had a cache-key mismatch between
writer and invalidator:
- `getChats` set the cache at
`mainChat:${req.headers['companyid']}`
- `updateMainChat` tried to invalidate at
`mainChat:${JSON.parse(JSON.stringify(result))?._id}`
β i.e. the updated document's `_id`, NOT the companyId.
The two keys never matched. `removeCache(...)` was a silent no-op and
stale chats stayed cached until the 3600s TTL expired.
Centralise the key in a small helper so both sites use the same shape:
const mainChatCacheKey = (companyId) => `mainChat:${companyId}`;
β¦and use it for both `myCache.set` and `removeCache`. The helper is
exported so the regression test can pin the shape.
Closes #74
Removes the backend npm ci, npm test, and npm audit steps from the validate job. Backend deps still install on the VPS at deploy time; validate is now a frontend build + artifact job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add release-please-config.json β tracks root package only, SemVer bump rules, CHANGELOG sections (user-facing types visible, engineering types hidden) - Add .release-please-manifest.json β anchors current version at 14.0.26 (matches existing v14.0.26 tag) - Add .github/workflows/release.yml β runs on push to main and workflow_dispatch; uses googleapis/release-please-action@v4 - Add CHANGELOG.md β bootstrapped with v14.0.26 baseline; release-please appends new sections from here Step 4 of the open-source repo maintenance baseline initiative. How it works: when commits land on main (via the staging promotion PR from BRANCHING.md), release-please opens a Release PR with the version bump and grouped Conventional-Commits changelog. Merging that PR creates the git tag, GitHub Release, and commits the bumps. Frontend versioning (8.36.0) stays independent for now; can be added as a second release-please component later if a unified version is desired. Zero new npm dependencies β the action runs entirely on GitHub runners. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Rewrite Dockerfile as a multi-stage build on node:20-alpine (~150MB vs the previous ~1GB), npm ci for reproducibility, non-root user, tini for signal handling, HEALTHCHECK via Node - Drop broken admin/ references from the old Dockerfile (admin module is removed from the open-source repo) - Rewrite docker-compose.yml as a complete stack: MongoDB service, persistent volumes (mongo data + app uploads), env_file, health checks, depends_on service_healthy gate, restart policies, configurable port, dedicated bridge network - Expand .dockerignore: add .git, .github, .env*, .claude, *.md, tests, coverage, logs, IDE/OS files β smaller build context and prevents accidental secrets leak - Add .github/workflows/docker.yml: multi-arch builds (amd64+arm64), pushes to GHCR on release-published (from release-please in #212) and on main pushes, PR builds run image verification only (no push). Docker Hub publishing is commented out β uncomment after adding DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repo secrets. Step 5 of the open-source repo maintenance baseline initiative. After this lands and the first release-please release is cut, end users will be able to: docker pull ghcr.io/aliansoftwareteam/alianhub:latest docker compose up -d Image name uses the short branded form (aliansoftwareteam/alianhub) rather than the auto-derived full repo path. Zero new npm dependencies β the workflow runs entirely on GitHub runners. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Docker build on PR #213 failed with: ERROR: failed to compute cache key: failed to calculate checksum of ref ...: "/migrations": not found Same class of error as the broken admin/ reference in the previous Dockerfile β over-specified folders that don't actually exist at the repo root. - Remove COPY locale/ β locales live under frontend/src/locales/ - Remove COPY migrations/ β migrate-mongo is a dep but no migrations folder exists yet - Add COPY socket/ β Socket.io runtime files - Add COPY public/ β Express static assets All 9 COPY'd folders verified present in the working tree. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The frontend build fails with:
Module not found: Error: Can't resolve '../../../../../package.json'
in '/app/frontend/src/components/organisms/Header'
frontend/src/components/organisms/Header/Header.vue imports
{version} from "../../../../../package.json" β i.e. the root
package.json, used to render the app version in the UI header.
The frontend-builder stage only copied frontend/package.json into
/app/frontend/. Adding COPY package.json /app/package.json so the
5-level relative import resolves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Don't run the Docker workflow during routine staging-targeted PR work. It now triggers only when: - A PR targets main (typically the staging->main promotion PR) - A commit lands on main - A release is published - workflow_dispatch (manual) Trade-off: Docker breakage won't be caught until the staging->main promotion PR opens. Acceptable since Docker files rarely change in day-to-day work. Easy to revert by re-adding 'staging' to pull_request.branches if frequent Docker changes become a pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
README: - Add 3 GitHub-native badges (release, discussions, stars) - Add hero screenshot under the badges row - Convert .gitbook/* image paths to absolute raw URLs so images render in non-GitHub viewers (markdownlivepreview, npm, Docker Hub, external aggregators) - New Quick Start with TWO options: Docker (recommended) + From source - New Architecture section with a Mermaid diagram (Vue/Electron -> Express+Socket.io -> MongoDB + Wasabi/local storage) - Expand Key Features from 6 bare bullets to 9 descriptive paragraphs covering project mgmt, RBAC + multi-tenancy, real-time collab, search + saved filters, timesheets, AI, chat, web+desktop, and self-hosting - New πΈ Screenshots section with 8 subsections: Dashboard / Board (Kanban) / List / Calendar / Task Detail / Workload Report / Settings & Customization / AI Assist - Rename "Getting Started" -> "Documentation" with cleaner per-file links - New Roadmap section pointing to new ROADMAP.md - Rewrite Contributing section with BRANCHING.md link and good-first-issue / help-wanted shortcuts - New Support & Community section with where-do-I-go matrix - New Repo Activity section using GitHub-native shields, with a Repobeats upgrade-path comment for later CONTRIBUTING.md: - New Commit Message Format section explaining Conventional Commits, accepted types, and npm run lint:commits New files: - ROADMAP.md (75 lines) β public roadmap template with Recently shipped / In progress / Planned / Considering / Out of scope sections - SUPPORT.md (86 lines) β routing matrix (docs, Discussions, Issues, Security, Commercial), bug-report content guide, response expectations - .gitbook/assets/screenshots/*.png β 8 product screenshots (1.2 MB total) Step 6 of the open-source repo maintenance baseline initiative. The License section and badge, the Branch Naming Convention in CONTRIBUTING.md, and the default PR template are intentionally left untouched to avoid conflicts with PRs #207, #208, and #209. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 7 of the open-source repo maintenance baseline initiative β
and the final one.
package.json:
- Add description (marketing-grade single-line pitch)
- Add 24 keywords for GitHub repo search and any future npm
publish (project-management, self-hosted, kanban, gantt,
multi-tenant, jira-alternative, asana-alternative,
monday-alternative, etc.)
- Add homepage, repository, bugs metadata fields
New file .claude/DISCOVERABILITY.md (internal, 464 lines):
- Audit of current GitHub repo metadata (description, 17 topics
already set β)
- 3 high-value topic additions with ready gh repo edit commands
- Submission targets ranked by ROI with ready-to-paste copy for
awesome-selfhosted, alternativeto.net, Product Hunt,
Hacker News (Show HN), Reddit (r/selfhosted, r/opensource,
r/coolgithubprojects), Dev.to + Hashnode launch outline,
LibHunt, opensource.builders, Indie Hackers
- SEO snippets for alianhub.com:
Core meta tags (title, description, OG, Twitter Cards)
schema.org SoftwareApplication JSON-LD markup
Sitemap & robots.txt reminders
- Tracking checklist with markdown checkboxes for every channel
- Maintenance cadence (once, per release, quarterly, annually)
- "What we deliberately skipped" with reasoning
.claude/REFERENCES.md: added DISCOVERABILITY.md to docs index.
Quality checks applied per code review:
- Meta description shortened from 215 to 140 chars (Google
SERP truncation fit)
- r/projectmanagement post replaced with SKIP recommendation
(high mod-removal risk; safer subreddits called out instead)
- HN tactics updated to 2026 reality (30+ upvotes/hour bar)
- og-image.png upgraded to BLOCKER with explicit creation guide
The playbook documents intent only β no external submissions or
repo-metadata changes are auto-executed. All gh repo edit commands
are ready to copy-paste after merge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
β¦l-3.0 chore(license): relicense from MIT to AGPL-3.0-or-later
docs(branching): add branching strategy and update CONTRIBUTING
- Remove standalone hotfix checkbox from PR template (not a
commitlint-accepted type; note in comment explains hotfix
branches should still use `fix` for their commits/PR title)
- Pin 3 GitHub Actions to commit SHAs (supply-chain hardening):
amannn/action-semantic-pull-request -> v5.5.3 SHA
actions/checkout -> v4.2.2 SHA
actions/setup-node -> v4.1.0 SHA
- Add persist-credentials: false to checkout step (prevents
GITHUB_TOKEN from leaking into subsequent steps)
- Match Node version to package.json engines: 22 -> 20
(eliminates EBADENGINE warnings)
- Make lint:commits work for forks/PRs against different bases:
hardcoded origin/staging -> ${BASE_BRANCH:-origin/staging}
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fixes CI failure on PR #209: npm error EUSAGE `npm ci` can only install packages when your package.json and package-lock.json are in sync. Missing: @commitlint/cli@^19.5.0, @commitlint/config-conventional@^19.5.0 (and ~50 transitive deps). When the commitlint devDeps were added to package.json in the initial commit, package-lock.json wasn't regenerated. `npm ci` runs in strict mode and refused to install packages not in the lockfile. Generated via: npm install --package-lock-only Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
β¦s-lint ci(lint): enforce Conventional Commits and branch-name conventions
ci(release): add release-please automation
docs(readme): polish README + add ROADMAP, SUPPORT, 8 screenshots
docs(discoverability): submission playbook + package.json metadata
ci(docker): publish multi-arch images to GHCR + complete compose stack
|
Important Review skippedToo many files! This PR contains 292 files, which is 142 over the limit of 150. To get a review, narrow the scope: βοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: β Files ignored due to path filters (8)
π Files selected for processing (292)
You can disable this status message by setting the Use the checkbox below for a quick retry:
β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Comment |
5 tasks
PR #216 (staging -> main, the first promotion) surfaced two gaps in our PR-validation workflows: 1. branch-name.yml validates the source branch against a fixed list of patterns. `staging` and `main` (used as source branches in promotion PRs and hotfix-merge-back flows) were not in the allowlist, so PR #216 failed the check. 2. commitlint.yml runs commitlint against every commit in the PR. For promotion PRs that aggregate hundreds of legacy commits (e.g. PR #216 brings 269 commits, many predating Conventional Commits enforcement), the check always fails. Both fixes are scoped to promotion-PR scenarios β they do NOT weaken validation for normal topic-branch PRs. branch-name.yml: - Add LONG_RUNNING='^(staging|main)$' to the allow-set, mirroring the documented promotion flow in BRANCHING.md - Update the help text to surface the staging|main option commitlint.yml: - Add `if: github.head_ref != 'staging' && github.head_ref != 'main'` to the lint-commits job, skipping per-commit validation when the source is a long-running branch - The PR title check (lint-pr-title) is unchanged β it still runs on every PR including promotion PRs, since the squash commit on merge uses the PR title Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
β¦218) Docker workflow no longer runs on pull_request events. Triggers are now scoped to: - release: published (release-please tags) - push to main (after a PR merges) - workflow_dispatch (manual run from Actions UI) Rationale: multi-arch docker builds (amd64 + arm64 via QEMU emulation) take 12-18 minutes on cold cache, 3-6 minutes on warm cache. Running them on every PR (even with the paths-filter scoping to docker-related files) blocks PR review feedback and slows the iteration loop. Trade-off accepted: - Docker breakage is now caught one step later: after a PR merges to main, not during PR review. - Easy to revert with a follow-up PR if a breaking change lands on main. - release.yml will fail loudly if the post-merge image build is broken, so we still get a hard signal before tagging a release. Docker files (Dockerfile, .dockerignore, docker-compose.yml, docker.yml) change rarely β most PRs are unaffected by this change. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joshishiv4
approved these changes
Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Template Chooser
Please click the link that matches your contribution type to load the correct format.
Bug Fix
Use this for fixing broken logic or UI glitches.
New Feature
Use this for adding new functionality or components.
Refactor
Use this for code cleanup, performance tweaks, or technical debt.
General Summary
If you don't want to use a specific template, please provide a brief summary of your changes below.