diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9533b72 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..4132143 Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/workflows/.DS_Store b/.github/workflows/.DS_Store new file mode 100644 index 0000000..c7f5fe4 Binary files /dev/null and b/.github/workflows/.DS_Store differ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef90624 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Semantic Release Workflow + +on: + push: + branches: + - main + - develop + - staging + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20.8.1" + + - name: Clean dependencies + run: rm -rf node_modules package-lock.json + + - name: Install dependencies + run: npm install + + - name: Debug environment + run: | + echo "JIRA_HOST: $JIRA_HOST" + echo "JIRA_EMAIL: $JIRA_EMAIL" + echo "JIRA_API_TOKEN: [hidden]" + echo "JIRA_PROJECT_KEY: $JIRA_PROJECT_KEY" + env: + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT_KEY }} + + - name: Run Semantic Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT_KEY }} + run: npx semantic-release diff --git a/jira-plugin.js b/jira-plugin.js new file mode 100644 index 0000000..812914a --- /dev/null +++ b/jira-plugin.js @@ -0,0 +1,96 @@ +const axios = require("axios"); + +module.exports = { + verifyConditions: async (pluginConfig, { logger }) => { + logger.log("Verifying Jira conditions..."); + if (!process.env.JIRA_HOST || !process.env.JIRA_EMAIL || !process.env.JIRA_API_TOKEN) { + throw new Error("Jira credentials are missing (JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN)."); + } + }, + + success: async (pluginConfig, context) => { + const { nextRelease, lastRelease, commits, branch, logger } = context; + const jiraHost = process.env.JIRA_HOST; + const jiraEmail = process.env.JIRA_EMAIL; + const jiraApiToken = process.env.JIRA_API_TOKEN; + const projectId = process.env.JIRA_PROJECT_KEY; // Example: SSP + + // Only process RC or stable branches + if (!["main", "staging"].includes(branch.name)) { + logger.log(`Skipping Fix Version update for branch ${branch.name}.`); + return; + } + + const releaseType = branch.name === "main" ? "stable" : "rc"; + + logger.log(`Processing commits since the last release: ${lastRelease.gitTag || "none"}`); + + // Extract Jira ticket IDs from new commits + const ticketRegex = /\b([A-Z]+-\d+)\b/g; + const ticketIds = Array.from( + new Set( + commits + .filter((commit) => commit.message) // Ensure the commit has a message + .map((commit) => commit.message.match(ticketRegex)) // Extract ticket IDs + .flat() + .filter(Boolean) // Remove null or undefined matches + ) + ); + + if (ticketIds.length === 0) { + logger.log("No Jira tickets found in new commits."); + return; + } + + logger.log(`Found Jira tickets: ${ticketIds.join(", ")}`); + + // Create or update the Fix Version in Jira + const fixVersion = { + name: nextRelease.version, + description: `Release ${nextRelease.version} (${releaseType})`, + released: releaseType === "stable", // Mark as released for stable releases + releaseDate: new Date().toISOString().split("T")[0], // Format: YYYY-MM-DD + }; + + try { + // Check if Fix Version exists + const { data: existingVersions } = await axios.get( + `${jiraHost}/rest/api/2/project/${projectId}/versions`, + { auth: { username: jiraEmail, password: jiraApiToken } } + ); + + const existingVersion = existingVersions.find((v) => v.name === fixVersion.name); + let versionId; + + if (existingVersion) { + versionId = existingVersion.id; + logger.log(`Fix Version "${fixVersion.name}" already exists.`); + } else { + // Create Fix Version + const { data: newVersion } = await axios.post( + `${jiraHost}/rest/api/2/version`, + { ...fixVersion, project: projectId }, + { auth: { username: jiraEmail, password: jiraApiToken } } + ); + versionId = newVersion.id; + logger.log(`Created Fix Version "${fixVersion.name}".`); + } + + // Update tickets with the Fix Version + await Promise.all( + ticketIds.map((ticketId) => + axios.put( + `${jiraHost}/rest/api/2/issue/${ticketId}`, + { fields: { fixVersions: [{ id: versionId }] } }, + { auth: { username: jiraEmail, password: jiraApiToken } } + ) + ) + ); + + logger.log(`Updated Fix Version for tickets: ${ticketIds.join(", ")}`); + } catch (error) { + logger.error("Failed to update Jira Fix Versions:", error.message); + throw error; + } + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..083b55c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "devDependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^11.0.1", + "conventional-changelog-conventionalcommits": "^8.0.0", + "execa": "^9.5.1", + "semantic-release": "^24.2.0", + "semantic-release-jira-releases": "^0.7.2" + }, + "dependencies": { + "axios": "^1.7.8" + } +} diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..9e0c6cd --- /dev/null +++ b/release.config.js @@ -0,0 +1,46 @@ +module.exports = { + branches: [ + { name: "main" }, // Stable production releases + { name: "develop", prerelease: "beta" }, // Beta pre-releases for ongoing development + { name: "staging", prerelease: "rc" }, // RC (release candidate) pre-releases + ], + plugins: [ + + [ + "@semantic-release/commit-analyzer", + { + preset: "conventionalcommits", + releaseRules: [ + { type: "feat", release: "minor" }, // Features trigger minor release + { type: "fix", release: "patch" }, // Fixes trigger patch release + { breaking: true, release: "major" }, // Breaking changes trigger major release + { type: "docs", release: false }, // Documentation changes are ignored + { type: "chore", release: false }, // Chores are ignored + ], + parserOpts: { + noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"], + }, + }, + ], + + "@semantic-release/release-notes-generator", + + [ + "@semantic-release/changelog", + { + changelogFile: "CHANGELOG.md", + }, + ], + + [ + "@semantic-release/git", + { + assets: ["CHANGELOG.md"], + message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", + }, + ], + + "@semantic-release/github", + "./jira-plugin", + ], +};