From e289990f0a9d29f7b1bac42a42f3e5dc618321eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:43:06 +0000 Subject: [PATCH 1/4] Initial plan From 3919a72437d2b53a8a7e6b976b2b95734cef467b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:46:03 +0000 Subject: [PATCH 2/4] Initial analysis of release notes synchronization issue Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2bd03a..d8d811e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "copilot-token-tracker", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-token-tracker", - "version": "0.0.1", + "version": "0.0.2", "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "24.x", From e444fdf59c53cb55344d358ba09096412c7cb34f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:51:16 +0000 Subject: [PATCH 3/4] feat: add automated release notes synchronization - Add GitHub workflow to sync CHANGELOG.md with GitHub releases - Add Node.js script for manual synchronization - Support both GitHub CLI and API token authentication - Include test mode for local development - Add npm scripts for easy usage - Update documentation with sync instructions Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- .github/workflows/sync-release-notes.yml | 179 ++++++++++++++++ CHANGELOG.md.backup | 18 ++ README.md | 22 ++ package.json | 4 +- scripts/sync-changelog.js | 250 +++++++++++++++++++++++ 5 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-release-notes.yml create mode 100644 CHANGELOG.md.backup create mode 100644 scripts/sync-changelog.js diff --git a/.github/workflows/sync-release-notes.yml b/.github/workflows/sync-release-notes.yml new file mode 100644 index 0000000..9cc4974 --- /dev/null +++ b/.github/workflows/sync-release-notes.yml @@ -0,0 +1,179 @@ +name: Sync Release Notes + +on: + workflow_dispatch: # Manual trigger + release: + types: [published, edited] # Automatic trigger when releases are published or edited + +jobs: + sync-release-notes: + runs-on: ubuntu-latest + permissions: + contents: write # Need write permission to update CHANGELOG.md + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 # Fetch all history so we can work with all releases + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '20.x' + + - name: Sync GitHub release notes to CHANGELOG.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Create a script to fetch GitHub releases and update CHANGELOG.md + cat > sync_changelog.js << 'EOF' + const { execSync } = require('child_process'); + const fs = require('fs'); + + async function syncReleaseNotes() { + try { + console.log('Fetching GitHub releases...'); + + // Fetch all releases using GitHub CLI + const releasesJson = execSync('gh release list --json tagName,name,body,createdAt,isPrerelease --limit 50', { encoding: 'utf8' }); + const releases = JSON.parse(releasesJson); + + console.log(`Found ${releases.length} releases`); + + // Sort releases by creation date (newest first) + releases.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + // Read current CHANGELOG.md + let changelog = ''; + if (fs.existsSync('CHANGELOG.md')) { + changelog = fs.readFileSync('CHANGELOG.md', 'utf8'); + } + + // Extract the header and unreleased section + const lines = changelog.split('\n'); + const headerEndIndex = lines.findIndex(line => line.startsWith('## [Unreleased]')); + const unreleasedEndIndex = lines.findIndex((line, index) => + index > headerEndIndex && line.startsWith('## [') && !line.includes('Unreleased') + ); + + let header = ''; + let unreleasedSection = ''; + + if (headerEndIndex >= 0) { + header = lines.slice(0, headerEndIndex + 1).join('\n'); + if (unreleasedEndIndex >= 0) { + unreleasedSection = lines.slice(headerEndIndex + 1, unreleasedEndIndex).join('\n'); + } else { + // Take everything after unreleased header until we find a release or end + const restOfFile = lines.slice(headerEndIndex + 1); + const nextReleaseIndex = restOfFile.findIndex(line => line.startsWith('## [') && !line.includes('Unreleased')); + if (nextReleaseIndex >= 0) { + unreleasedSection = restOfFile.slice(0, nextReleaseIndex).join('\n'); + } else { + unreleasedSection = restOfFile.join('\n'); + } + } + } else { + // Create basic header if none exists + header = '# Change Log\n\nAll notable changes to the "copilot-token-tracker" extension will be documented in this file.\n\nCheck [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.\n\n## [Unreleased]'; + unreleasedSection = '\n'; + } + + // Build new changelog content + let newChangelog = header + unreleasedSection + '\n'; + + // Add releases + for (const release of releases) { + const version = release.tagName.startsWith('v') ? release.tagName.substring(1) : release.tagName; + const releaseType = release.isPrerelease ? ' - Pre-release' : ''; + + newChangelog += `## [${version}]${releaseType}\n\n`; + + if (release.body && release.body.trim()) { + // Clean up the release body + let body = release.body.trim(); + + // Remove any "Full Changelog" links at the end + body = body.replace(/\*\*Full Changelog\*\*:.*$/gm, '').trim(); + + // Ensure bullet points are properly formatted + const bodyLines = body.split('\n').map(line => { + line = line.trim(); + if (line && !line.startsWith('-') && !line.startsWith('*') && !line.startsWith('#')) { + return `- ${line}`; + } + return line; + }).filter(line => line.length > 0); + + newChangelog += bodyLines.join('\n') + '\n\n'; + } else { + newChangelog += `- Release ${version}\n\n`; + } + } + + // Write the new changelog + fs.writeFileSync('CHANGELOG.md', newChangelog.trim() + '\n'); + console.log('CHANGELOG.md updated successfully!'); + + // Show what changed + try { + const diff = execSync('git diff CHANGELOG.md', { encoding: 'utf8' }); + if (diff.trim()) { + console.log('\nChanges made to CHANGELOG.md:'); + console.log(diff); + } else { + console.log('No changes needed - CHANGELOG.md is already up to date'); + } + } catch (error) { + console.log('Could not show diff, but file was updated'); + } + + } catch (error) { + console.error('Error syncing release notes:', error); + process.exit(1); + } + } + + syncReleaseNotes(); + EOF + + # Run the sync script + node sync_changelog.js + + - name: Check for changes + id: changes + run: | + if git diff --quiet CHANGELOG.md; then + echo "changed=false" >> $GITHUB_OUTPUT + echo "No changes detected in CHANGELOG.md" + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "Changes detected in CHANGELOG.md" + fi + + - name: Commit and push changes + if: steps.changes.outputs.changed == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add CHANGELOG.md + git commit -m "docs: sync CHANGELOG.md with GitHub release notes + + This commit automatically updates the CHANGELOG.md file to match + the release notes from GitHub releases, ensuring consistency + between local documentation and published releases." + git push + + - name: Summary + run: | + if [ "${{ steps.changes.outputs.changed }}" == "true" ]; then + echo "โœ… CHANGELOG.md has been successfully updated with GitHub release notes" + else + echo "โ„น๏ธ CHANGELOG.md was already up to date with GitHub release notes" + fi \ No newline at end of file diff --git a/CHANGELOG.md.backup b/CHANGELOG.md.backup new file mode 100644 index 0000000..a44b88e --- /dev/null +++ b/CHANGELOG.md.backup @@ -0,0 +1,18 @@ +# Change Log + +All notable changes to the "copilot-token-tracker" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Automated VSIX build and release workflow + +## [0.0.1] - Initial Release + +- Initial release +- Real-time token tracking with status bar display +- Automatic updates every 5 minutes +- Click to refresh functionality +- Smart estimation using character-based analysis +- Detailed view with comprehensive statistics \ No newline at end of file diff --git a/README.md b/README.md index 2a0b159..891883a 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,25 @@ The release workflow will: **Note**: The workflow will fail if the tag version doesn't match the version in `package.json`. +### Syncing Release Notes + +To keep the local `CHANGELOG.md` file synchronized with GitHub release notes: + +**Manual Sync:** +```bash +npm run sync-changelog +``` + +**Automatic Sync:** +The project includes a GitHub workflow that automatically updates `CHANGELOG.md` whenever: +- A new release is published +- An existing release is edited +- The workflow is manually triggered + +**Test the Sync:** +```bash +npm run sync-changelog:test +``` + +This ensures that the local changelog always reflects the latest release information from GitHub, preventing the documentation from becoming outdated. + diff --git a/package.json b/package.json index 5768292..5dafcd6 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "pretest": "npm run compile-tests && npm run compile && npm run lint", "check-types": "tsc --noEmit", "lint": "eslint src", - "test": "vscode-test" + "test": "vscode-test", + "sync-changelog": "node scripts/sync-changelog.js", + "sync-changelog:test": "node scripts/sync-changelog.js --test" }, "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/scripts/sync-changelog.js b/scripts/sync-changelog.js new file mode 100644 index 0000000..b48b8e3 --- /dev/null +++ b/scripts/sync-changelog.js @@ -0,0 +1,250 @@ +#!/usr/bin/env node + +/** + * Sync CHANGELOG.md with GitHub release notes + * + * This script fetches GitHub release notes and updates the local CHANGELOG.md file + * to ensure consistency between local documentation and published releases. + * + * Usage: + * node scripts/sync-changelog.js [--test] + * + * Options: + * --test Use hardcoded test data instead of fetching from GitHub + * + * Requirements: + * - GitHub CLI (gh) installed and authenticated OR GITHUB_TOKEN environment variable + * - Run from the repository root directory + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const TEST_MODE = process.argv.includes('--test'); + +// Test data matching the actual GitHub releases +const TEST_RELEASES = [ + { + tagName: "v0.0.2", + name: "Release 0.0.2", + body: "\n- Automated VSIX build and release workflow", + createdAt: "2025-09-28T12:31:58Z", + isPrerelease: false + }, + { + tagName: "v0.0.1", + name: "First draft", + body: "First rough version, not complete of course! \r\n\r\n- Only tested on windows\r\n- Use at your own risk ๐Ÿ˜„\r\n- Screenshots in the README\r\n- VS Code v1.104 or higher\r\n\r\n**Full Changelog**: https://github.com/rajbos/github-copilot-token-usage/commits/v0.0.1", + createdAt: "2025-09-26T21:55:29Z", + isPrerelease: true + } +]; + +async function fetchGitHubReleases() { + if (TEST_MODE) { + console.log('๐Ÿงช Using test data (--test mode)...'); + return TEST_RELEASES; + } + + // Try GitHub CLI first + try { + execSync('gh --version', { stdio: 'ignore' }); + console.log('๐Ÿ“ก Fetching GitHub releases using GitHub CLI...'); + const releasesJson = execSync('gh release list --json tagName,name,body,createdAt,isPrerelease --limit 50', { encoding: 'utf8' }); + return JSON.parse(releasesJson); + } catch (error) { + console.log('โš ๏ธ GitHub CLI not available or not authenticated, falling back to GitHub API...'); + } + + // Fall back to GitHub API + const token = process.env.GITHUB_TOKEN; + if (!token) { + console.error('โŒ Error: GitHub CLI is not available and GITHUB_TOKEN environment variable is not set'); + console.error(' Please either:'); + console.error(' 1. Install and authenticate GitHub CLI: https://cli.github.com/'); + console.error(' 2. Set GITHUB_TOKEN environment variable with a GitHub personal access token'); + console.error(' 3. Use --test flag to test with sample data'); + throw new Error('No authentication method available'); + } + + // Extract repository info from package.json + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const repoUrl = packageJson.repository?.url || ''; + const match = repoUrl.match(/github\.com[\/:](.+?)\/(.+?)(?:\.git)?$/); + if (!match) { + throw new Error('Could not extract repository information from package.json'); + } + + const [, owner, repo] = match; + console.log(`๐Ÿ“ก Fetching releases for ${owner}/${repo} using GitHub API...`); + + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + port: 443, + path: `/repos/${owner}/${repo}/releases?per_page=50`, + method: 'GET', + headers: { + 'Authorization': `token ${token}`, + 'User-Agent': 'changelog-sync-script', + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(`GitHub API returned ${res.statusCode}: ${data}`)); + return; + } + + const apiReleases = JSON.parse(data); + // Convert API format to CLI format + const releases = apiReleases.map(release => ({ + tagName: release.tag_name, + name: release.name, + body: release.body, + createdAt: release.created_at, + isPrerelease: release.prerelease + })); + + resolve(releases); + }); + }); + + req.on('error', reject); + req.end(); + }); +} + +async function syncReleaseNotes() { + try { + console.log('๐Ÿ”„ Syncing CHANGELOG.md with GitHub release notes...'); + + // Check if we're in the right directory + if (!fs.existsSync('package.json')) { + console.error('โŒ Error: This script must be run from the repository root directory'); + process.exit(1); + } + + const releases = await fetchGitHubReleases(); + + console.log(`๐Ÿ“‹ Found ${releases.length} releases`); + + if (releases.length === 0) { + console.log('โ„น๏ธ No releases found. Nothing to sync.'); + return; + } + + // Sort releases by creation date (newest first) + releases.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + // Read current CHANGELOG.md + let changelog = ''; + const changelogPath = 'CHANGELOG.md'; + if (fs.existsSync(changelogPath)) { + changelog = fs.readFileSync(changelogPath, 'utf8'); + console.log('๐Ÿ“– Reading existing CHANGELOG.md'); + } else { + console.log('๐Ÿ“ CHANGELOG.md does not exist, creating new file'); + } + + // Extract the header and unreleased section + const lines = changelog.split('\n'); + const headerEndIndex = lines.findIndex(line => line.startsWith('## [Unreleased]')); + const unreleasedEndIndex = lines.findIndex((line, index) => + index > headerEndIndex && line.startsWith('## [') && !line.includes('Unreleased') + ); + + let header = ''; + let unreleasedSection = ''; + + if (headerEndIndex >= 0) { + header = lines.slice(0, headerEndIndex + 1).join('\n'); + if (unreleasedEndIndex >= 0) { + unreleasedSection = lines.slice(headerEndIndex + 1, unreleasedEndIndex).join('\n'); + } else { + // Take everything after unreleased header until we find a release or end + const restOfFile = lines.slice(headerEndIndex + 1); + const nextReleaseIndex = restOfFile.findIndex(line => line.startsWith('## [') && !line.includes('Unreleased')); + if (nextReleaseIndex >= 0) { + unreleasedSection = restOfFile.slice(0, nextReleaseIndex).join('\n'); + } else { + unreleasedSection = restOfFile.join('\n'); + } + } + } else { + // Create basic header if none exists + header = '# Change Log\n\nAll notable changes to the "copilot-token-tracker" extension will be documented in this file.\n\nCheck [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.\n\n## [Unreleased]'; + unreleasedSection = '\n'; + } + + // Build new changelog content + let newChangelog = header + unreleasedSection + '\n'; + + console.log('โœ๏ธ Building changelog entries from releases...'); + + // Add releases + for (const release of releases) { + const version = release.tagName.startsWith('v') ? release.tagName.substring(1) : release.tagName; + const releaseType = release.isPrerelease ? ' - Pre-release' : ''; + + newChangelog += `## [${version}]${releaseType}\n\n`; + + if (release.body && release.body.trim()) { + // Clean up the release body + let body = release.body.trim(); + + // Remove any "Full Changelog" links at the end + body = body.replace(/\*\*Full Changelog\*\*:.*$/gm, '').trim(); + + // Ensure bullet points are properly formatted + const bodyLines = body.split('\n').map(line => { + line = line.trim(); + if (line && !line.startsWith('-') && !line.startsWith('*') && !line.startsWith('#')) { + return `- ${line}`; + } + return line; + }).filter(line => line.length > 0); + + newChangelog += bodyLines.join('\n') + '\n\n'; + } else { + newChangelog += `- Release ${version}\n\n`; + } + } + + // Write the new changelog + fs.writeFileSync(changelogPath, newChangelog.trim() + '\n'); + console.log('๐Ÿ’พ CHANGELOG.md updated successfully!'); + + // Show what changed + try { + const diff = execSync('git diff CHANGELOG.md', { encoding: 'utf8' }); + if (diff.trim()) { + console.log('\n๐Ÿ“Š Changes made to CHANGELOG.md:'); + console.log(diff); + console.log('\nโœ… Sync completed successfully! Review the changes and commit them when ready.'); + } else { + console.log('โ„น๏ธ No changes needed - CHANGELOG.md is already up to date'); + } + } catch (error) { + console.log('๐Ÿ’ก Could not show diff, but file was updated'); + console.log('โœ… Sync completed successfully!'); + } + + } catch (error) { + console.error('โŒ Error syncing release notes:', error.message); + process.exit(1); + } +} + +// Run the sync if this script is executed directly +if (require.main === module) { + syncReleaseNotes(); +} + +module.exports = { syncReleaseNotes }; \ No newline at end of file From f810ed6471f5240e489c350186e8fa9d65861bdf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:52:45 +0000 Subject: [PATCH 4/4] chore: add backup files to gitignore and finalize sync implementation Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- .gitignore | 1 + CHANGELOG.md.backup | 18 ------------------ 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 CHANGELOG.md.backup diff --git a/.gitignore b/.gitignore index 0b60dfa..20bd014 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist node_modules .vscode-test/ *.vsix +*.backup diff --git a/CHANGELOG.md.backup b/CHANGELOG.md.backup deleted file mode 100644 index a44b88e..0000000 --- a/CHANGELOG.md.backup +++ /dev/null @@ -1,18 +0,0 @@ -# Change Log - -All notable changes to the "copilot-token-tracker" extension will be documented in this file. - -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. - -## [Unreleased] - -- Automated VSIX build and release workflow - -## [0.0.1] - Initial Release - -- Initial release -- Real-time token tracking with status bar display -- Automatic updates every 5 minutes -- Click to refresh functionality -- Smart estimation using character-based analysis -- Detailed view with comprehensive statistics \ No newline at end of file