#include "Workflow.hpp" #include #include "Caps.hpp" namespace molo::example::fw { bool parse_commit(const std::string& subject, const std::string& body, CommitInfo& out, std::string& error) { std::string s = subject; if (s.size() > (std::size_t)caps::kMaxCommitSubject) { error = "subject too long"; return false; } std::size_t colon = s.find(':'); if (colon == std::string::npos) { error = "missing colon"; return false; } std::string head = s.substr(0, colon); out.description = s.substr(colon + 1); while (!out.description.empty() && out.description.front() == ' ') out.description.erase(out.description.begin()); if (!head.empty() && head.back() == '!') { out.breaking = true; head.pop_back(); } std::size_t paren = head.find('('); if (paren == std::string::npos) { out.type = head; } else { out.type = head.substr(0, paren); std::size_t close = head.find(')', paren); if (close == std::string::npos) { error = "unclosed scope"; return false; } out.scope = head.substr(paren + 1, close - paren - 1); } const char* allowed[] = {"feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"}; bool ok = false; for (const char* t : allowed) { if (out.type == t) { ok = true; break; } } if (!ok) { error = "unknown type: " + out.type; return false; } if (body.find("BREAKING CHANGE") != std::string::npos) out.breaking = true; return true; } std::string bump_name(Bump bump) { switch (bump) { case Bump::None: return "none"; case Bump::Patch: return "patch"; case Bump::Minor: return "minor"; case Bump::Major: return "major"; } return "none"; } Bump ReleasePlanner::decide(const std::vector& commits) const { bool feat = false, fix = false; for (const auto& c : commits) { if (c.breaking) return Bump::Major; if (c.type == "feat") feat = true; if (c.type == "fix") fix = true; } if (feat) return Bump::Minor; if (fix) return Bump::Patch; return Bump::None; } std::string ReleasePlanner::next_version( const std::string& current, const std::vector& commits) const { int major = 0, minor = 0, patch = 0; std::sscanf(current.c_str(), "%d.%d.%d", &major, &minor, &patch); switch (decide(commits)) { case Bump::Major: return std::to_string(major + 1) + ".0.0"; case Bump::Minor: return std::to_string(major) + "." + std::to_string(minor + 1) + ".0"; case Bump::Patch: return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch + 1); case Bump::None: return current; } return current; } std::string ChangelogWriter::entry( const std::string& version, const std::string& date, const std::vector& commits) const { std::string added, changed, fixed, removed; for (const auto& c : commits) { std::string line = "- " + (c.scope.empty() ? "" : "(" + c.scope + ") ") + c.description + "\n"; if (c.breaking) changed += line; else if (c.type == "feat") added += line; else if (c.type == "fix") fixed += line; else changed += line; } std::string out = "## [" + version + "] - " + date + "\n"; if (!added.empty()) out += "### Added\n" + added; if (!changed.empty()) out += "### Changed\n" + changed; if (!fixed.empty()) out += "### Fixed\n" + fixed; if (!removed.empty()) out += "### Removed\n" + removed; int lines = 0; for (char c : out) { if (c == '\n') ++lines; } if (lines > caps::kMaxChangelogLines) out += "(truncated to cap)\n"; return out; } void ReleaseVerifier::add_check(std::string name, bool passed, std::string detail) { checks_.push_back({std::move(name), passed, std::move(detail)}); } bool ReleaseVerifier::green() const { if (checks_.empty()) return false; for (const auto& c : checks_) { if (!c.passed) return false; } return true; } std::vector ReleaseVerifier::checks() const { return checks_; } } // namespace molo::example::fw