diff --git a/AUTO_VERSION.md b/AUTO_VERSION.md index 57b2589..4bf70cf 100644 --- a/AUTO_VERSION.md +++ b/AUTO_VERSION.md @@ -4,6 +4,9 @@ The auto-versioning feature is designed to manage product firmware version numbers in an automated and consistent manner. +It is primarily intended for use in Action workflows that release firmware and upload it to Particle products. +Here is an [example firmware project](https://github.com/particle-iot/firmware-cicd-examples/tree/main/product-firmware) that has a two GitHub Actions workflows: build and upload. + ## Usage Auto-versioning is disabled by default. To enable auto-versioning: @@ -188,3 +191,8 @@ jobs: 1. Manual Version Changes: If you manually increment the version macro while automatic versioning is enabled, the automatic versioning system may increment the version again. It is recommended that you disable automatic versioning if you are going to manually increment the version macro. + +## Debugging + +To debug the auto-versioning feature, you can [re-run a job with debug logging](https://github.blog/changelog/2022-05-24-github-actions-re-run-jobs-with-debug-logging/) enabled. +This will allow you to see the step-by-step process of how the version number is determined and incremented. diff --git a/README.md b/README.md index 3c0ab00..96f41c7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Other Actions for firmware development: Compile | [Flash Device](https://github. # Required: true particle-platform-name: '' - # Path to directory with sources to compile + # This is your Particle project directory + # It contains your source code, libraries, and the project.properties file # Required: false sources-folder: '.' diff --git a/dist/index.js b/dist/index.js index 34fcf64..9acdb50 100644 --- a/dist/index.js +++ b/dist/index.js @@ -34104,7 +34104,6 @@ exports.compileAction = compileAction; "use strict"; -// Auto revision assumes the PRODUCT_VERSION macro is only incremented and not decremented. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -34114,13 +34113,68 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.isProductFirmware = exports.incrementVersion = exports.shouldIncrementVersion = void 0; const promises_1 = __nccwpck_require__(3292); const core_1 = __nccwpck_require__(2186); +const simple_git_1 = __importDefault(__nccwpck_require__(9103)); const git_1 = __nccwpck_require__(6350); +const git = (0, simple_git_1.default)(); +// Detailed Git repo state logging functions, for debugging git state in the Action runner +function logGitStatus(gitRepo) { + return __awaiter(this, void 0, void 0, function* () { + try { + const status = yield git.cwd(gitRepo).status(); + (0, core_1.debug)(`Git Status: ${JSON.stringify(status)}`); + } + catch (e) { + (0, core_1.error)(`Error getting Git status: ${e}`); + } + }); +} +function logGitBranches(gitRepo) { + return __awaiter(this, void 0, void 0, function* () { + try { + const branches = yield git.cwd(gitRepo).branchLocal(); + (0, core_1.debug)(`Local branches: ${JSON.stringify(branches)}`); + } + catch (e) { + (0, core_1.error)(`Error listing branches: ${e}`); + } + }); +} +function logGitCommitHistory(gitRepo, filePath) { + return __awaiter(this, void 0, void 0, function* () { + try { + const log = yield git.cwd(gitRepo).log({ file: filePath }); + (0, core_1.debug)(`Git log for ${filePath}: ${JSON.stringify(log)}`); + } + catch (e) { + (0, core_1.error)(`Error getting Git log for file ${filePath}: ${e}`); + } + }); +} +function getChangedFilesBetweenCommits(gitRepo, commit1, commit2) { + return __awaiter(this, void 0, void 0, function* () { + try { + const diffSummary = yield git.cwd(gitRepo).diffSummary([commit1, commit2]); + return diffSummary.files.map(file => file.file); + } + catch (e) { + (0, core_1.error)(`Error getting changed files between commits ${commit1} and ${commit2}: ${e}`); + return []; + } + }); +} function shouldIncrementVersion({ gitRepo, sources, productVersionMacroName }) { return __awaiter(this, void 0, void 0, function* () { + (0, core_1.debug)(`Starting shouldIncrementVersion for productVersionMacroName: ${productVersionMacroName} in repo: ${gitRepo}`); + // Additional debugging around Git repo state at the start + yield logGitStatus(gitRepo); + yield logGitBranches(gitRepo); const versionFilePath = yield (0, git_1.findProductVersionMacroFile)({ sources, productVersionMacroName @@ -34128,17 +34182,26 @@ function shouldIncrementVersion({ gitRepo, sources, productVersionMacroName }) { if (!versionFilePath) { throw new Error('Could not find a file containing the version macro.'); } + yield logGitCommitHistory(gitRepo, versionFilePath); const lastChangeRevision = yield (0, git_1.revisionOfLastVersionBump)({ gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); + (0, core_1.debug)(`Last change revision: ${lastChangeRevision}`); const currentSourcesRevision = yield (0, git_1.mostRecentRevisionInFolder)({ gitRepo: gitRepo, folderPath: sources }); + (0, core_1.debug)(`Current sources revision: ${currentSourcesRevision}`); + // Additional debugging around changed files + if (lastChangeRevision !== currentSourcesRevision) { + const changedFiles = yield getChangedFilesBetweenCommits(gitRepo, lastChangeRevision, currentSourcesRevision); + (0, core_1.debug)(`Files changed between ${lastChangeRevision} and ${currentSourcesRevision}: ${JSON.stringify(changedFiles)}`); + } const currentProductVersion = yield (0, git_1.currentFirmwareVersion)({ gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); + (0, core_1.debug)(`Current product version: ${currentProductVersion}`); if (!lastChangeRevision) { throw new Error('Could not find the last version increment.'); } @@ -34148,6 +34211,7 @@ function shouldIncrementVersion({ gitRepo, sources, productVersionMacroName }) { (0, core_1.warning)('The file with the product version macro has uncommitted changes.'); } const shouldIncrement = currentSourcesRevision !== lastChangeRevision; + (0, core_1.debug)(`Should increment version: ${shouldIncrement}`); if (!shouldIncrement) { (0, core_1.info)('No version increment detected. Skipping version increment.'); return false; @@ -34159,25 +34223,33 @@ function shouldIncrementVersion({ gitRepo, sources, productVersionMacroName }) { exports.shouldIncrementVersion = shouldIncrementVersion; function incrementVersion({ gitRepo, sources, productVersionMacroName }) { return __awaiter(this, void 0, void 0, function* () { - // find the file containing the version macro + (0, core_1.debug)(`Starting incrementVersion for productVersionMacroName: ${productVersionMacroName} in repo: ${gitRepo}`); const versionFilePath = yield (0, git_1.findProductVersionMacroFile)({ - sources: sources, + sources, + productVersionMacroName + }); + (0, core_1.debug)(`Version file path for incrementing: ${versionFilePath}`); + const current = yield (0, git_1.currentFirmwareVersion)({ + gitRepo: gitRepo, + versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); - // get the current version - const current = yield (0, git_1.currentFirmwareVersion)({ gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); - // increment the version + (0, core_1.debug)(`Current version before increment: ${current}`); const next = current + 1; - // find the line that matches this regex const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); - // Read the file content const fileContent = yield (0, promises_1.readFile)(versionFilePath, 'utf-8'); - // Replace the version with the next version + (0, core_1.debug)(`Read version file content from: ${versionFilePath}`); const updatedFileContent = fileContent.replace(versionRegex, (match, p1) => { (0, core_1.info)(`Replacing ${p1} with ${next} in ${versionFilePath}`); + (0, core_1.debug)(`Match found for version increment: ${match}`); return match.replace(p1, next.toString()); }); yield (0, promises_1.writeFile)(versionFilePath, updatedFileContent); + (0, core_1.debug)(`Version file updated: ${versionFilePath}`); + // Additional debugging around Git repo state at the end + // A successful version increment should leave a modified file in the repo + // Users should commit and push the updated version file to git after `compile-action` finishes + yield logGitStatus(gitRepo); return { file: versionFilePath, version: next @@ -34187,14 +34259,17 @@ function incrementVersion({ gitRepo, sources, productVersionMacroName }) { exports.incrementVersion = incrementVersion; function isProductFirmware({ sources, productVersionMacroName }) { return __awaiter(this, void 0, void 0, function* () { + (0, core_1.debug)(`Checking if product firmware for productVersionMacroName: ${productVersionMacroName}`); let isProductFirmware = false; try { isProductFirmware = !!(yield (0, git_1.findProductVersionMacroFile)({ sources: sources, productVersionMacroName: productVersionMacroName })); + (0, core_1.debug)(`Product firmware status: ${isProductFirmware}`); } - catch (error) { + catch (err) { + (0, core_1.debug)(`Error in isProductFirmware: ${err}`); // Ignore } return isProductFirmware; @@ -34323,6 +34398,7 @@ exports.hasFullHistory = exports.mostRecentRevisionInFolder = exports.findNeares const promises_1 = __nccwpck_require__(3292); const path_1 = __nccwpck_require__(1017); const simple_git_1 = __importDefault(__nccwpck_require__(9103)); +const core_1 = __nccwpck_require__(2186); function currentFirmwareVersion({ gitRepo, versionFilePath, productVersionMacroName }) { return __awaiter(this, void 0, void 0, function* () { const git = (0, simple_git_1.default)(gitRepo); @@ -34339,14 +34415,23 @@ function currentFirmwareVersion({ gitRepo, versionFilePath, productVersionMacroN } for (const log of logs.all) { const currentCommit = log.hash; - const commitBody = yield git.show([`${currentCommit}:${versionFilePath}`]); + (0, core_1.debug)(`Looking for the file ${versionFilePath} in commit ${currentCommit}`); // Use regex to extract the PRODUCT_VERSION from the patch const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); + let commitBody = ''; + try { + commitBody = yield git.show([`${currentCommit}:${versionFilePath}`]); + } + catch (error) { + (0, core_1.debug)(`Error getting the file ${versionFilePath} from commit ${currentCommit}: ${error}. This can occur if the file was deleted in the commit. Skipping commit`); + } const match = versionRegex.exec(commitBody); if (match) { + (0, core_1.debug)(`Found the ${productVersionMacroName} macro at commit ${currentCommit} with version ${match[1]}`); const currentVersion = parseInt(match[1], 10); // Check if the current version is higher than the previous version and higher than the highest version found if (currentVersion > highestVersion) { + (0, core_1.debug)(`Found a new highest version: ${currentVersion} at commit ${currentCommit}`); highestVersion = currentVersion; } } @@ -34401,6 +34486,7 @@ function findProductVersionMacroFile({ sources, productVersionMacroName }) { const fileContent = yield (0, promises_1.readFile)(fullPath, 'utf-8'); const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); if (fileContent && versionRegex.test(fileContent)) { + (0, core_1.debug)(`Found the ${productVersionMacroName} macro in the file ${fullPath}`); return fullPath; } } diff --git a/src/autoversion.test.ts b/src/autoversion.test.ts index 60f7884..9bb9b1c 100644 --- a/src/autoversion.test.ts +++ b/src/autoversion.test.ts @@ -19,9 +19,13 @@ jest.mock('fs/promises', () => ({ const warningMock = jest.fn(); const infoMock = jest.fn(); +const debugMock = jest.fn(); +const errorMock = jest.fn(); jest.mock('@actions/core', () => ({ warning: warningMock, - info: infoMock + info: infoMock, + debug: debugMock, + error: errorMock })); import { findProductVersionMacroFile, currentFirmwareVersion } from './git'; diff --git a/src/autoversion.ts b/src/autoversion.ts index 858fe35..987cbab 100644 --- a/src/autoversion.ts +++ b/src/autoversion.ts @@ -1,7 +1,6 @@ -// Auto revision assumes the PRODUCT_VERSION macro is only incremented and not decremented. - import { readFile, writeFile } from 'fs/promises'; -import { info, warning } from '@actions/core'; +import { info, warning, debug, error } from '@actions/core'; +import simpleGit, { SimpleGit } from 'simple-git'; import { currentFirmwareVersion, findProductVersionMacroFile, @@ -9,6 +8,46 @@ import { revisionOfLastVersionBump } from './git'; +const git: SimpleGit = simpleGit(); + +// Detailed Git repo state logging functions, for debugging git state in the Action runner +async function logGitStatus(gitRepo: string): Promise { + try { + const status = await git.cwd(gitRepo).status(); + debug(`Git Status: ${JSON.stringify(status)}`); + } catch (e) { + error(`Error getting Git status: ${e}`); + } +} + +async function logGitBranches(gitRepo: string): Promise { + try { + const branches = await git.cwd(gitRepo).branchLocal(); + debug(`Local branches: ${JSON.stringify(branches)}`); + } catch (e) { + error(`Error listing branches: ${e}`); + } +} + +async function logGitCommitHistory(gitRepo: string, filePath: string): Promise { + try { + const log = await git.cwd(gitRepo).log({ file: filePath }); + debug(`Git log for ${filePath}: ${JSON.stringify(log)}`); + } catch (e) { + error(`Error getting Git log for file ${filePath}: ${e}`); + } +} + +async function getChangedFilesBetweenCommits(gitRepo: string, commit1: string, commit2: string): Promise { + try { + const diffSummary = await git.cwd(gitRepo).diffSummary([commit1, commit2]); + return diffSummary.files.map(file => file.file); + } catch (e) { + error(`Error getting changed files between commits ${commit1} and ${commit2}: ${e}`); + return []; + } +} + export async function shouldIncrementVersion( { gitRepo, sources, productVersionMacroName }: { gitRepo: string; @@ -16,6 +55,12 @@ export async function shouldIncrementVersion( productVersionMacroName: string; } ): Promise { + debug(`Starting shouldIncrementVersion for productVersionMacroName: ${productVersionMacroName} in repo: ${gitRepo}`); + + // Additional debugging around Git repo state at the start + await logGitStatus(gitRepo); + await logGitBranches(gitRepo); + const versionFilePath = await findProductVersionMacroFile({ sources, productVersionMacroName @@ -24,17 +69,30 @@ export async function shouldIncrementVersion( throw new Error('Could not find a file containing the version macro.'); } + await logGitCommitHistory(gitRepo, versionFilePath); + const lastChangeRevision = await revisionOfLastVersionBump({ gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); + debug(`Last change revision: ${lastChangeRevision}`); + const currentSourcesRevision = await mostRecentRevisionInFolder({ gitRepo: gitRepo, folderPath: sources }); + debug(`Current sources revision: ${currentSourcesRevision}`); + + // Additional debugging around changed files + if (lastChangeRevision !== currentSourcesRevision) { + const changedFiles = await getChangedFilesBetweenCommits(gitRepo, lastChangeRevision, currentSourcesRevision); + debug(`Files changed between ${lastChangeRevision} and ${currentSourcesRevision}: ${JSON.stringify(changedFiles)}`); + } + const currentProductVersion = await currentFirmwareVersion({ gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName }); + debug(`Current product version: ${currentProductVersion}`); if (!lastChangeRevision) { throw new Error('Could not find the last version increment.'); @@ -42,15 +100,19 @@ export async function shouldIncrementVersion( info(`Current firmware version: ${currentProductVersion} (${currentSourcesRevision})`); info(`Firmware version last set at: ${lastChangeRevision}`); + if (lastChangeRevision === '00000000') { warning('The file with the product version macro has uncommitted changes.'); } const shouldIncrement = currentSourcesRevision !== lastChangeRevision; + debug(`Should increment version: ${shouldIncrement}`); + if (!shouldIncrement) { info('No version increment detected. Skipping version increment.'); return false; } + info(`Incrementing firmware version to ${currentProductVersion + 1}.`); return true; } @@ -64,33 +126,42 @@ export async function incrementVersion( file: string; version: number }> { - // find the file containing the version macro + debug(`Starting incrementVersion for productVersionMacroName: ${productVersionMacroName} in repo: ${gitRepo}`); + const versionFilePath = await findProductVersionMacroFile({ - sources: sources, - productVersionMacroName: productVersionMacroName + sources, + productVersionMacroName }); - // get the current version - const current = await currentFirmwareVersion( - { gitRepo: gitRepo, versionFilePath: versionFilePath, productVersionMacroName: productVersionMacroName } - ); + debug(`Version file path for incrementing: ${versionFilePath}`); + + const current = await currentFirmwareVersion({ + gitRepo: gitRepo, + versionFilePath: versionFilePath, + productVersionMacroName: productVersionMacroName + }); - // increment the version + debug(`Current version before increment: ${current}`); const next = current + 1; - // find the line that matches this regex const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); - // Read the file content const fileContent = await readFile(versionFilePath, 'utf-8'); + debug(`Read version file content from: ${versionFilePath}`); - // Replace the version with the next version const updatedFileContent = fileContent.replace(versionRegex, (match, p1) => { info(`Replacing ${p1} with ${next} in ${versionFilePath}`); + debug(`Match found for version increment: ${match}`); return match.replace(p1, next.toString()); }); await writeFile(versionFilePath, updatedFileContent); + debug(`Version file updated: ${versionFilePath}`); + + // Additional debugging around Git repo state at the end + // A successful version increment should leave a modified file in the repo + // Users should commit and push the updated version file to git after `compile-action` finishes + await logGitStatus(gitRepo); return { file: versionFilePath, @@ -98,19 +169,22 @@ export async function incrementVersion( }; } - export async function isProductFirmware( { sources, productVersionMacroName }: { sources: string; productVersionMacroName: string; }): Promise { + debug(`Checking if product firmware for productVersionMacroName: ${productVersionMacroName}`); + let isProductFirmware = false; try { isProductFirmware = !!await findProductVersionMacroFile({ sources: sources, productVersionMacroName: productVersionMacroName }); - } catch (error) { + debug(`Product firmware status: ${isProductFirmware}`); + } catch (err) { + debug(`Error in isProductFirmware: ${err}`); // Ignore } return isProductFirmware; diff --git a/src/git.test.ts b/src/git.test.ts index 8af43ed..7932c86 100644 --- a/src/git.test.ts +++ b/src/git.test.ts @@ -167,6 +167,26 @@ describe('currentFirmwareVersion', () => { expect(result).toBe(0); }); + + test('should handle when the version file was deleted in a previous commit', async () => { + const commitHashes = ['a1b2c3d4e5f6', 'b2c3d4e5f6a1']; + const gitRepo = '/path/to/repo'; + const versionFilePath = '/path/to/repo/project-folder/application.cpp'; + const productVersionMacroName = 'PRODUCT_VERSION'; + + logMock.mockResolvedValue(createLogMock(commitHashes)); + showMock + .mockRejectedValueOnce(new Error(`path '${versionFilePath}' exists on disk, but not in '${commitHashes[0]}'`)) + .mockResolvedValueOnce(createCommitBodyMock(productVersionMacroName, 1)); + + const result = await currentFirmwareVersion({ + gitRepo: gitRepo, + versionFilePath: versionFilePath, + productVersionMacroName: productVersionMacroName + }); + + expect(result).toBe(1); + }); }); describe('findProductVersionMacroFile', () => { diff --git a/src/git.ts b/src/git.ts index 4e6bd8b..af1e3ce 100644 --- a/src/git.ts +++ b/src/git.ts @@ -1,6 +1,7 @@ import { readdir, readFile, stat } from 'fs/promises'; import { dirname, join } from 'path'; import simpleGit, { SimpleGit } from 'simple-git'; +import { debug } from '@actions/core'; export async function currentFirmwareVersion( { gitRepo, versionFilePath, productVersionMacroName }: { @@ -27,18 +28,27 @@ export async function currentFirmwareVersion( for (const log of logs.all) { const currentCommit = log.hash; - const commitBody = await git.show([`${currentCommit}:${versionFilePath}`]); + debug(`Looking for the file ${versionFilePath} in commit ${currentCommit}`); // Use regex to extract the PRODUCT_VERSION from the patch const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); + let commitBody = ''; + try { + commitBody = await git.show([`${currentCommit}:${versionFilePath}`]); + } catch (error) { + debug(`Error getting the file ${versionFilePath} from commit ${currentCommit}: ${error}. This can occur if the file was deleted in the commit. Skipping commit`); + } const match = versionRegex.exec(commitBody); if (match) { + debug(`Found the ${productVersionMacroName} macro at commit ${currentCommit} with version ${match[1]}`); + const currentVersion = parseInt(match[1], 10); // Check if the current version is higher than the previous version and higher than the highest version found if (currentVersion > highestVersion) { + debug(`Found a new highest version: ${currentVersion} at commit ${currentCommit}`); highestVersion = currentVersion; } } @@ -100,6 +110,7 @@ export async function findProductVersionMacroFile( const fileContent = await readFile(fullPath, 'utf-8'); const versionRegex = new RegExp(`^.*${productVersionMacroName}.*\\((\\d+)\\)`, 'gm'); if (fileContent && versionRegex.test(fileContent)) { + debug(`Found the ${productVersionMacroName} macro in the file ${fullPath}`); return fullPath; } }