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
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,59 @@ jobs:
cli.js cli/ lib/ plugins/ web-ui.html web-ui/ \
node_modules/ package.json LICENSE README.md README.zh.md
echo "STANDALONE_TGZ=$name" >> "$GITHUB_ENV"
- name: Fetch contributors from GitHub API
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
LATEST_TAG: ${{ steps.resolve.outputs.latest_tag }}
CONTRIBUTORS_FILE: release-contributors.txt
Comment on lines +198 to +199

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use the same baseline tag as changelog generation.

Line 224 derives PRs from LATEST_TAG...RELEASE_TAG, but changelog generation uses the previous semver tag for the target release. This mismatch can produce contributor data for a different range.

Also applies to: 224-224

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 198 - 199, The LATEST_TAG
environment variable is set to steps.resolve.outputs.latest_tag, but this
baseline differs from what the changelog generation uses for deriving the target
release, causing a mismatch in the contributor data range. Update the LATEST_TAG
assignment to use the same baseline tag that the changelog generation process
uses (the previous semver tag for the target release) instead of
steps.resolve.outputs.latest_tag, ensuring both operations work with the same
tag range.

run: |
if [ -z "${LATEST_TAG}" ]; then
echo "::notice title=No previous tag::Skipping contributors fetch for initial release."
echo "" > "${CONTRIBUTORS_FILE}"
exit 0
fi

if ! command -v gh >/dev/null 2>&1; then
echo "::error title=gh CLI not found::GitHub CLI is required."
exit 1
fi

tmp_logins=$(mktemp)
trap 'rm -f "${tmp_logins}"' EXIT

# Fetch PR authors in range using base...head comparison
gh pr list \
--repo "${GITHUB_REPOSITORY}" \
--limit 500 \
--json author \
--jq '.[].author.login' 2>/dev/null | sort -u > "${tmp_logins}" || true
Comment on lines +216 to +220

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are the default filters for gh pr list (state, scope), and does this command support constraining results to commits between two git tags?

💡 Result:

The default state for the gh pr list command is open [1][2][3]. Regarding your specific questions: Default Filters: The gh pr list command filters by state, and its default value is open [1][2][4]. The available states you can filter by are open, closed, merged, and all [1][3][4]. There is no scope parameter in the command itself; however, you can constrain results to a specific repository using the -R or --repo flag [2][3]. Constraint by Git Tags: The gh pr list command does not natively support constraining results to commits between two git tags [1][2][4]. While you can use the --search flag to pass advanced GitHub search queries—such as searching by a specific commit SHA—it does not support range-based filtering using git tags directly [2][5][4]. To filter pull requests based on commits or time ranges associated with tags, you would typically need to identify the commit SHAs corresponding to your tags (using standard git commands like git rev-list or git show) and then use those SHAs or the associated dates within a --search query [2][4][6]. For example, if you have the commit SHAs, you might use the search syntax within the -S flag, though this requires manual translation of your git tags into inputs the search API accepts [2][4].

Citations:


🏁 Script executed:

sed -n '200,250p' .github/workflows/release.yml

Repository: SakuraByteCore/codexmate

Length of output: 2220


Fix the unscoped contributor fallback in release workflow.

The gh pr list call on line 216 lacks range filtering and defaults to all open PRs in the repository. When the primary range-scoped mechanism (lines 227-234, which extracts PR numbers from git log) finds no PRs, the fallback on lines 235-236 injects these unscoped contributors into the release contributors list, compromising data accuracy.

Replace the fallback to keep the contributor list empty with a notice instead:

Suggested patch
-          elif [ -s "${tmp_logins}" ]; then
-            cat "${tmp_logins}" > "${CONTRIBUTORS_FILE}"
+          elif [ -s "${tmp_logins}" ]; then
+            echo "::notice title=No range-scoped PR contributors::Using empty contributor list."
+            : > "${CONTRIBUTORS_FILE}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 216 - 220, The fallback mechanism
around lines 235-236 injects unscoped contributors from the gh pr list command
into the release contributors list when the primary range-scoped extraction
(from git log in lines 227-234) finds no results. Instead of using the unscoped
gh pr list output as a fallback, modify the fallback logic to keep the
contributor list empty or add a notice indicating that no contributors were
found within the intended range. This ensures data accuracy by preventing
contributors outside the target range from being included in the release.


# Fetch merged PRs in range using commits
tmp_merged=$(mktemp)
git log "${LATEST_TAG}...${RELEASE_TAG}" --pretty=format:%s \
| grep -oE '#[0-9]+' \
| sed 's/^#//' \
| sort -u \
| while read -r pr_number; do
gh pr view "${pr_number}" --repo "${GITHUB_REPOSITORY}" --json author --jq '.author.login' 2>/dev/null || true
done \
| sort -u > "${tmp_merged}" || true
Comment on lines +224 to +231

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use an existing ref for the commit-range upper bound.

On Line 224, git log "${LATEST_TAG}...${RELEASE_TAG}" can fail when ${RELEASE_TAG} hasn’t been created yet (new release flow). Because the pipeline ends with || true, this silently empties tmp_merged and skews contributors downstream.

Suggested fix
+          range_head="${RELEASE_TAG}"
+          if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}" >/dev/null; then
+            range_head="HEAD"
+          fi
+
-          git log "${LATEST_TAG}...${RELEASE_TAG}" --pretty=format:%s \
+          git log "${LATEST_TAG}...${range_head}" --pretty=format:%s \
             | grep -oE '#[0-9]+' \
             | sed 's/^`#//`' \
             | sort -u \
             | while read -r pr_number; do
                 gh pr view "${pr_number}" --repo "${GITHUB_REPOSITORY}" --json author --jq '.author.login' 2>/dev/null || true
               done \
             | sort -u > "${tmp_merged}" || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
git log "${LATEST_TAG}...${RELEASE_TAG}" --pretty=format:%s \
| grep -oE '#[0-9]+' \
| sed 's/^#//' \
| sort -u \
| while read -r pr_number; do
gh pr view "${pr_number}" --repo "${GITHUB_REPOSITORY}" --json author --jq '.author.login' 2>/dev/null || true
done \
| sort -u > "${tmp_merged}" || true
range_head="${RELEASE_TAG}"
if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}" >/dev/null; then
range_head="HEAD"
fi
git log "${LATEST_TAG}...${range_head}" --pretty=format:%s \
| grep -oE '#[0-9]+' \
| sed 's/^`#//`' \
| sort -u \
| while read -r pr_number; do
gh pr view "${pr_number}" --repo "${GITHUB_REPOSITORY}" --json author --jq '.author.login' 2>/dev/null || true
done \
| sort -u > "${tmp_merged}" || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 224 - 231, The git log command
using the commit range "${LATEST_TAG}...${RELEASE_TAG}" fails when RELEASE_TAG
hasn't been created yet in a new release flow. Since the failure is silently
handled by the || true operator, tmp_merged becomes empty, causing incorrect
contributor data downstream. Replace ${RELEASE_TAG} in the git log command with
an existing ref that is guaranteed to be present (such as HEAD or the target
branch reference) to ensure the command succeeds regardless of whether the
release tag has been created yet.


if [ -s "${tmp_merged}" ]; then
cat "${tmp_merged}" > "${CONTRIBUTORS_FILE}"
elif [ -s "${tmp_logins}" ]; then
cat "${tmp_logins}" > "${CONTRIBUTORS_FILE}"
else
echo "::notice title=No contributors found::No contributors in this range."
echo "" > "${CONTRIBUTORS_FILE}"
fi
- name: Generate release notes from actual commit range
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
TAG_EXISTS: ${{ steps.resolve.outputs.tag_exists }}
RELEASE_CHANGELOG_FILE: release-changelog.md
CONTRIBUTORS_FILE: release-contributors.txt
run: |
if [ "${TAG_EXISTS}" = "true" ] && [ ! -f tools/release/changelog.js ]; then
echo "::notice title=Release changelog skipped::tools/release/changelog.js is not present in existing tag ${RELEASE_TAG}."
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/release-changelog.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('release changelog groups PR commits and direct commits for action logs', (
const grouped = groupCommits(commits);

assert.deepStrictEqual(grouped.directCommits.map((commit) => commit.hash), ['f5700cf', 'abc1234']);
assert.deepStrictEqual(listContributors(commits), ['awsl233777']);
assert.deepStrictEqual(listContributors(commits), [{ login: 'awsl233777', displayName: 'Awsl' }]);

const changelog = formatChangelog({
repository: 'SakuraByteCore/codexmate',
Expand Down
51 changes: 40 additions & 11 deletions tools/release/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,13 @@ function contributorProfile(author) {
return { login: displayName, displayName };
}

function formatContributorCard(author) {
const { login, displayName } = contributorProfile(author);
function formatContributorCard(authorOrObj) {
let login, displayName;
if (typeof authorOrObj === 'object' && authorOrObj !== null) {
({ login, displayName } = authorOrObj);
} else {
({ login, displayName } = contributorProfile(authorOrObj));
}
const safeLogin = encodeURIComponent(login);
const safeDisplayName = escapeHtml(displayName);
const githubAvatarUrl = `https://github.com/${safeLogin}.png?size=96`;
Expand All @@ -130,16 +135,26 @@ function formatContributorCard(author) {
].join('\n');
}

function listContributors(commits) {
function listContributors(commits, externalLogins = []) {
const seen = new Set();
const contributors = [];

// Use external logins from GitHub API if available
for (const login of externalLogins) {
const safeLogin = String(login || '').trim();
if (!safeLogin || seen.has(safeLogin)) continue;
seen.add(safeLogin);
contributors.push({ login: safeLogin, displayName: safeLogin });
}

// Fallback to commit authors for missing entries
for (const commit of commits) {
const contributor = formatContributorName(commit.author);
const key = contributor.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
contributors.push(contributor);
const { login, displayName } = contributorProfile(commit.author);
if (!login || seen.has(login)) continue;
seen.add(login);
Comment on lines +143 to +154

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize login keys before deduplication.

On Line 145 and Line 153, dedupe uses case-sensitive keys. This can still emit duplicates when the same GitHub login appears with different casing across sources.

Suggested patch
 for (const login of externalLogins) {
     const safeLogin = String(login || '').trim();
-    if (!safeLogin || seen.has(safeLogin)) continue;
-    seen.add(safeLogin);
+    const loginKey = safeLogin.toLowerCase();
+    if (!safeLogin || seen.has(loginKey)) continue;
+    seen.add(loginKey);
     contributors.push({ login: safeLogin, displayName: safeLogin });
 }
 ...
 for (const commit of commits) {
     const { login, displayName } = contributorProfile(commit.author);
-    if (!login || seen.has(login)) continue;
-    seen.add(login);
+    const loginKey = String(login || '').toLowerCase();
+    if (!login || seen.has(loginKey)) continue;
+    seen.add(loginKey);
     contributors.push({ login, displayName });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/release/changelog.js` around lines 143 - 154, The deduplication logic
uses case-sensitive keys when checking and adding to the seen Set, which allows
the same GitHub login to appear multiple times with different casing (e.g.,
"GitHub" vs "github"). Normalize the login values to lowercase before performing
any deduplication checks. In the first loop where externalLogins are processed,
convert safeLogin to lowercase before the seen.has() check and before
seen.add(). In the second loop where commits are processed, convert login to
lowercase before the seen.has() check and before seen.add(). Ensure the
normalized lowercase value is used consistently for deduplication while
preserving the original casing for the displayName property.

contributors.push({ login, displayName });
}

return contributors;
}

Expand Down Expand Up @@ -179,7 +194,7 @@ function formatChangeSummary(commits) {
return lines;
}

function formatChangelog({ repository = '', previousTag = '', currentTag = '', currentRef = 'HEAD', commits = [] }) {
function formatChangelog({ repository = '', previousTag = '', currentTag = '', currentRef = 'HEAD', commits = [], externalLogins = [] }) {
const lines = [];

if (!previousTag) {
Expand Down Expand Up @@ -217,7 +232,7 @@ function formatChangelog({ repository = '', previousTag = '', currentTag = '', c
}

lines.push('### Contributors');
const contributors = listContributors(commits);
const contributors = listContributors(commits, externalLogins);
if (!contributors.length) {
lines.push('- Unknown contributor');
} else {
Expand Down Expand Up @@ -248,12 +263,26 @@ function main(env = process.env) {
const previousTag = selectPreviousSemverTag(tags, currentTag);
const currentRef = resolveCurrentRef(currentTag);
const commits = previousTag ? readCommits(previousTag, currentRef) : [];

// Load external logins from GitHub API if available
let externalLogins = [];
const contributorsFile = env.CONTRIBUTORS_FILE;
if (contributorsFile && fs.existsSync(contributorsFile)) {
try {
const content = fs.readFileSync(contributorsFile, 'utf8');
externalLogins = content.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean);
} catch (e) {
console.warn(`Failed to read contributors file: ${e.message}`);
}
}

const changelog = formatChangelog({
repository: env.GITHUB_REPOSITORY || '',
previousTag,
currentTag,
currentRef,
commits
commits,
externalLogins
});

console.log(changelog.trimEnd());
Expand Down
6 changes: 0 additions & 6 deletions web-ui/modules/i18n/locales/vi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1180,10 +1180,6 @@ const vi = Object.freeze({
'settings.tab.data': 'Dữ liệu',
'settings.tabs.aria': 'Danh mục cài đặt',
'settings.quickSettings.title': 'Cài đặt nhanh',
'settings.sharePrefix.title': 'Tiền tố lệnh chia sẻ',
'settings.sharePrefix.meta': 'Dùng làm tiền tố cho "Sao chép lệnh chia sẻ" trong Web UI',
'settings.sharePrefix.label': 'Tiền tố',
'settings.sharePrefix.hint': 'Mặc định là npm start (local). Bạn có thể chuyển sang codexmate toàn cục. Cài đặt này được lưu trong trình duyệt.',
'settings.claude.title': 'Cấu hình Claude',
'settings.claude.meta': 'Sao lưu / nhập ~/.claude',
'settings.codex.title': 'Cấu hình Codex',
Expand Down Expand Up @@ -1219,8 +1215,6 @@ const vi = Object.freeze({
'settings.trash.retentionMeta': 'Mục trong thùng rác cũ hơn số ngày lưu giữ sẽ tự động bị xóa',
'settings.trash.retentionLabel': 'Số ngày lưu giữ',
'settings.trash.retentionHint': 'Phạm vi 1-365 ngày, mặc định 30. Mục hết hạn bị xóa khi tải thùng rác.',
'settings.trashConfig.title': 'Cấu hình thùng rác',
'settings.trashConfig.meta': 'Bật/tắt thùng rác và số ngày tự động dọn dẹp',
'settings.templateConfirm.title': 'Xác nhận áp dụng template',
'settings.templateConfirm.meta': 'Giảm ghi nhầm',
'settings.templateConfirm.toggle': 'Xem trước diff trước khi áp dụng (Xác nhận → Áp dụng)',
Expand Down
Loading