Skip to content

Commit 1d1f164

Browse files
Copilotpelikhan
andauthored
Auto-bump version when release tag already exists (#31592)
* Update Compute Release Config to bump version if tag already exists Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Address code review: clearer variable name, explicit switch, better error message Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore: initial plan Agent-Logs-Url: https://github.com/github/gh-aw/sessions/810b4de7-3401-41b3-a103-216d51e80760 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore: merge main and recompile workflow lock files Agent-Logs-Url: https://github.com/github/gh-aw/sessions/810b4de7-3401-41b3-a103-216d51e80760 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 6d136d0 commit 1d1f164

9 files changed

Lines changed: 301 additions & 1037 deletions

File tree

.github/workflows/release.lock.yml

Lines changed: 56 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/release.md

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,33 +111,55 @@ jobs:
111111
break;
112112
}
113113
114-
const releaseTag = `v${major}.${minor}.${patch}`;
115-
console.log(`Computed release tag: ${releaseTag}`);
116-
117-
// Sanity check: Verify the computed tag doesn't already exist
118-
const existingRelease = releases.find(r => r.tag_name === releaseTag);
119-
if (existingRelease) {
120-
core.setFailed(`Release tag ${releaseTag} already exists (created ${existingRelease.created_at}). Cannot create duplicate release. Please check existing releases.`);
121-
return;
114+
// Helper: check whether a given tag already exists (as a release or git ref)
115+
const tagExists = async (tagName) => {
116+
const releaseExists = releases.some(r => r.tag_name === tagName);
117+
if (releaseExists) return true;
118+
try {
119+
await github.rest.git.getRef({
120+
owner: context.repo.owner,
121+
repo: context.repo.repo,
122+
ref: `tags/${tagName}`
123+
});
124+
return true; // tag ref exists
125+
} catch (error) {
126+
if (error.status === 404) return false;
127+
throw new Error(`Failed to check if tag ${tagName} exists: ${error.message}`);
128+
}
129+
};
130+
131+
// Find the first available tag, bumping the minor/patch if the computed one is taken.
132+
// This handles the case where a release failed half-way and left a tag behind.
133+
const MAX_ATTEMPTS = 10;
134+
let releaseTag;
135+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
136+
const candidate = `v${major}.${minor}.${patch}`;
137+
if (!(await tagExists(candidate))) {
138+
releaseTag = candidate;
139+
break;
140+
}
141+
console.log(`Tag ${candidate} already exists – bumping version and retrying…`);
142+
// For patch releases keep bumping the patch number.
143+
// For minor/major releases bump the minor number (patch is already 0).
144+
switch (releaseType) {
145+
case 'patch':
146+
patch += 1;
147+
break;
148+
case 'minor':
149+
minor += 1;
150+
break;
151+
case 'major':
152+
minor += 1;
153+
break;
154+
}
122155
}
123-
124-
// Also check if tag exists in git (in case release was deleted but tag remains)
125-
try {
126-
await github.rest.git.getRef({
127-
owner: context.repo.owner,
128-
repo: context.repo.repo,
129-
ref: `tags/${releaseTag}`
130-
});
131-
// If we get here, the tag exists
132-
core.setFailed(`Git tag ${releaseTag} already exists in the repository. Cannot create duplicate tag. Please delete the existing tag or use a different version.`);
156+
157+
if (!releaseTag) {
158+
core.setFailed(`Could not find an available release tag after ${MAX_ATTEMPTS} attempts. Please check existing tags and releases.`);
133159
return;
134-
} catch (error) {
135-
// 404 means tag doesn't exist, which is what we want
136-
if (error.status !== 404) {
137-
throw error; // Re-throw unexpected errors
138-
}
139160
}
140-
161+
162+
console.log(`Computed release tag: ${releaseTag}`);
141163
core.setOutput('release_tag', releaseTag);
142164
console.log(`✓ Release tag: ${releaseTag}`);
143165
push_tag:

actions/setup/js/create_forecast_issue.cjs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ function buildForecastIssueBody(report, options) {
4242
const zeroProjectedWithSamples = rows.filter(([, sampledRuns, p50]) => Number(sampledRuns) > 0 && Number(p50) === 0).length;
4343
const zeroWorkflowWord = zeroProjectedWithSamples === 1 ? "workflow" : "workflows";
4444
const zeroWorkflowVerb = zeroProjectedWithSamples === 1 ? "has" : "have";
45-
const reportTable = [
46-
"| Workflow | Sampled runs | Forecast ET (P50) |",
47-
"| --- | ---: | ---: |",
48-
...rows.map(([workflowID, sampledRuns, p50]) => `| ${workflowID} | ${sampledRuns} | ${formatET(p50)} |`),
49-
].join("\n");
45+
const reportTable = ["| Workflow | Sampled runs | Forecast ET (P50) |", "| --- | ---: | ---: |", ...rows.map(([workflowID, sampledRuns, p50]) => `| ${workflowID} | ${sampledRuns} | ${formatET(p50)} |`)].join("\n");
5046

5147
const repoSlug = `${options.owner}/${options.repo}`;
5248
const period = report.period || "month";
@@ -72,11 +68,11 @@ function buildForecastIssueBody(report, options) {
7268
: []),
7369
...(zeroProjectedWithSamples > 0
7470
? [
75-
"> [!TIP]",
76-
`> ${zeroProjectedWithSamples} ${zeroWorkflowWord} ${zeroWorkflowVerb} sampled runs but forecast ET is 0. This usually indicates missing token usage in cached run summaries for sampled runs.`,
77-
"> Increase the warm-up scope with `gh aw logs --start-date -30d --count <larger value>` if this persists.",
78-
"",
79-
]
71+
"> [!TIP]",
72+
`> ${zeroProjectedWithSamples} ${zeroWorkflowWord} ${zeroWorkflowVerb} sampled runs but forecast ET is 0. This usually indicates missing token usage in cached run summaries for sampled runs.`,
73+
"> Increase the warm-up scope with `gh aw logs --start-date -30d --count <larger value>` if this persists.",
74+
"",
75+
]
8076
: []),
8177
...(runURL ? [`_Forecast source run: [#${runID}](${runURL})._`] : []),
8278
].join("\n");

actions/setup/js/create_forecast_issue.test.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe("create_forecast_issue", () => {
7171
serverUrl: "https://github.com",
7272
runID: "123456",
7373
generatedAtISO: "2026-01-01T00:00:00.000Z",
74-
},
74+
}
7575
);
7676

7777
expect(body).toContain("| Workflow | Sampled runs | Forecast ET (P50) |");
@@ -95,7 +95,7 @@ describe("create_forecast_issue", () => {
9595
repo: "repo",
9696
serverUrl: "https://github.com",
9797
generatedAtISO: "2026-01-01T00:00:00.000Z",
98-
},
98+
}
9999
);
100100

101101
expect(body).toContain("All projected ET values are 0 even after cache warm-up.");

actions/setup/js/link_sub_issue.test.cjs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,7 @@ const mockCore = {
126126
.mockResolvedValueOnce({ data: { number: 100, title: "Parent Issue", node_id: "I_parent_100", labels: [] } })
127127
.mockResolvedValueOnce({ data: { number: 50, title: "Sub Issue", node_id: "I_sub_50", labels: [] } });
128128
// Pre-flight check says no parent (e.g. parent field not yet populated), but mutation fails
129-
mockGithub.graphql
130-
.mockResolvedValueOnce({ repository: { issue: { parent: null } } })
131-
.mockRejectedValueOnce(new Error("Issue is already a sub-issue of the specified issue."));
129+
mockGithub.graphql.mockResolvedValueOnce({ repository: { issue: { parent: null } } }).mockRejectedValueOnce(new Error("Issue is already a sub-issue of the specified issue."));
132130

133131
const result = await handler(message, {});
134132

actions/setup/js/parse_token_usage.test.cjs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,7 @@ const fs = require("fs");
55
const path = require("path");
66
const os = require("os");
77

8-
const {
9-
main,
10-
getReadableTokenUsagePaths,
11-
extractRequestId,
12-
readDedupedTokenUsage,
13-
TOKEN_USAGE_AUDIT_PATH,
14-
TOKEN_USAGE_PATH,
15-
TOKEN_USAGE_PATHS,
16-
AGENT_USAGE_PATH,
17-
} = require("./parse_token_usage.cjs");
8+
const { main, getReadableTokenUsagePaths, extractRequestId, readDedupedTokenUsage, TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, AGENT_USAGE_PATH } = require("./parse_token_usage.cjs");
189

1910
describe("parse_token_usage", () => {
2011
const singleEntry = JSON.stringify({

0 commit comments

Comments
 (0)