Skip to content

Commit de4bc45

Browse files
authored
Allow multiple releases per day in the smithy-rs repository (#3875)
## Motivation and Context Currently, we can only make one `smithy-rs` release per day, and this restricts our ability to respond to urgent issues. This PR lifts that limitation, allowing us to make multiple releases per day. ## Description The core of this change is in the `render` subcommand of `changelogger`. When generating a date-based release tag, it now checks for existing tags on the same day. If a tag already exists, the `render` subcommand will append a numerical suffix to ensure the new tag is unique. In fact, appending a numerical suffix to make a release tag unique has been a workaround in our release pipeline (outside the `smithy-rs` repository) for quite some time. With the changes in this PR, we can eliminate that temporary solution from the release pipeline. Now that `changelogger` requires access to previous tags, CI steps that run `generate-smithy-rs-release` need to checkout the `smithy-rs` repository with all tags (`fetch-depth: 0` is for that purpose). ## Testing - [x] Added unit tests for `changelogger` - [x] Successfully bumped the release tag in [dry-run](https://github.com/smithy-lang/smithy-rs/actions/runs/11356509152/job/31588857360#step:8:26) (based on [this dummy change](cb19b31) to trick `changelogger` into thinking that it has to bump a release tag) - [x] Successfully bumped the release tag in the release pipeline (without the temporary hack we placed last year) ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent c8c610f commit de4bc45

File tree

13 files changed

+216
-273
lines changed

13 files changed

+216
-273
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ jobs:
6565
with:
6666
path: smithy-rs
6767
ref: ${{ inputs.git_ref }}
68+
# `generate-smithy-rs-release` requires access to previous tags to determine if a numerical suffix is needed
69+
# to make the release tag unique
70+
fetch-depth: 0
6871
# The models from aws-sdk-rust are needed to generate the full SDK for CI
6972
- uses: actions/checkout@v4
7073
with:

.github/workflows/release-scripts/create-release.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const assert = require("assert");
1010
const fs = require("fs");
1111

1212
const smithy_rs_repo = {
13-
owner: "awslabs",
13+
owner: "smithy-lang",
1414
repo: "smithy-rs",
1515
};
1616

.github/workflows/release.yml

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,18 @@ jobs:
181181
ref: ${{ inputs.commit_sha }}
182182
path: smithy-rs
183183
token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
184+
fetch-depth: 0
184185
- name: Generate release artifacts
185186
uses: ./smithy-rs/.github/actions/docker-build
186187
with:
187188
action: generate-smithy-rs-release
188189
- name: Download all artifacts
189190
uses: ./smithy-rs/.github/actions/download-all-artifacts
191+
# This step is not idempotent, as it pushes release artifacts to the `smithy-rs-release-1.x.y` branch. However,
192+
# if this step succeeds but a subsequent step fails, retrying the release workflow is "safe" in that it does not
193+
# create any inconsistent states; this step would simply fail because the release branch would be ahead of `main`
194+
# due to previously pushed artifacts.
195+
# To successfully retry a release, revert the commits in the release branch that pushed the artifacts.
190196
- name: Push smithy-rs changes
191197
shell: bash
192198
working-directory: smithy-rs-release/smithy-rs
@@ -202,7 +208,7 @@ jobs:
202208
# to retry a release action execution that failed due to a transient issue.
203209
# In that case, we expect the commit to be releasable as-is, i.e. the changelog should have already
204210
# been processed.
205-
git fetch --unshallow
211+
git fetch
206212
if [[ "${DRY_RUN}" == "true" ]]; then
207213
# During dry-runs, "git push" without "--force" can fail if smithy-rs-release-x.y.z-preview is behind
208214
# smithy-rs-release-x.y.z, but that does not matter much during dry-runs.
@@ -214,18 +220,7 @@ jobs:
214220
fi
215221
fi
216222
echo "commit_sha=$(git rev-parse HEAD)" > $GITHUB_OUTPUT
217-
- name: Tag release
218-
uses: actions/github-script@v7
219-
with:
220-
github-token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
221-
script: |
222-
const createReleaseScript = require("./smithy-rs/.github/workflows/release-scripts/create-release.js");
223-
await createReleaseScript({
224-
github,
225-
isDryRun: ${{ inputs.dry_run }},
226-
releaseManifestPath: "smithy-rs-release/smithy-rs-release-manifest.json",
227-
releaseCommitish: "${{ steps.push-changelog.outputs.commit_sha }}"
228-
});
223+
# This step is idempotent; the `publisher` will not publish a crate if the version is already published on crates.io.
229224
- name: Publish to crates.io
230225
shell: bash
231226
working-directory: smithy-rs-release/crates-to-publish
@@ -247,7 +242,23 @@ jobs:
247242
else
248243
publisher publish -y --location .
249244
fi
245+
# This step is not idempotent and MUST be performed last, as it will generate a new release in the `smithy-rs`
246+
# repository with the release tag that is always unique and has an increasing numerical suffix.
247+
- name: Tag release
248+
uses: actions/github-script@v7
249+
with:
250+
github-token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
251+
script: |
252+
const createReleaseScript = require("./smithy-rs/.github/workflows/release-scripts/create-release.js");
253+
await createReleaseScript({
254+
github,
255+
isDryRun: ${{ inputs.dry_run }},
256+
releaseManifestPath: "smithy-rs-release/smithy-rs-release-manifest.json",
257+
releaseCommitish: "${{ steps.push-changelog.outputs.commit_sha }}"
258+
});
250259
260+
# If this step fails for any reason, there's no need to retry the release workflow, as this step is auxiliary
261+
# and the release itself was successful. Instead, manually trigger `backport-pull-request.yml`.
251262
open-backport-pull-request:
252263
name: Open backport pull request to merge the release branch back to main
253264
needs:

tools/ci-build/changelogger/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/ci-build/changelogger/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "changelogger"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
authors = ["AWS Rust SDK Team <[email protected]>"]
55
description = "A CLI tool render and update changelogs from changelog files"
66
edition = "2021"

tools/ci-build/changelogger/src/main.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ mod tests {
8989
previous_release_versions_manifest: None,
9090
date_override: None,
9191
smithy_rs_location: None,
92+
aws_sdk_rust_location: None,
9293
})
9394
},
9495
Args::try_parse_from([
@@ -124,6 +125,7 @@ mod tests {
124125
previous_release_versions_manifest: None,
125126
date_override: None,
126127
smithy_rs_location: None,
128+
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
127129
})
128130
},
129131
Args::try_parse_from([
@@ -140,6 +142,8 @@ mod tests {
140142
"fromplace",
141143
"--changelog-output",
142144
"some-changelog",
145+
"--aws-sdk-rust-location",
146+
"aws-sdk-rust-location",
143147
])
144148
.unwrap()
145149
);
@@ -159,6 +163,7 @@ mod tests {
159163
)),
160164
date_override: None,
161165
smithy_rs_location: None,
166+
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
162167
})
163168
},
164169
Args::try_parse_from([
@@ -174,7 +179,9 @@ mod tests {
174179
"--changelog-output",
175180
"some-changelog",
176181
"--previous-release-versions-manifest",
177-
"path/to/versions.toml"
182+
"path/to/versions.toml",
183+
"--aws-sdk-rust-location",
184+
"aws-sdk-rust-location",
178185
])
179186
.unwrap()
180187
);
@@ -196,6 +203,7 @@ mod tests {
196203
)),
197204
date_override: None,
198205
smithy_rs_location: None,
206+
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
199207
})
200208
},
201209
Args::try_parse_from([
@@ -213,7 +221,9 @@ mod tests {
213221
"--current-release-versions-manifest",
214222
"path/to/current/versions.toml",
215223
"--previous-release-versions-manifest",
216-
"path/to/previous/versions.toml"
224+
"path/to/previous/versions.toml",
225+
"--aws-sdk-rust-location",
226+
"aws-sdk-rust-location",
217227
])
218228
.unwrap()
219229
);

tools/ci-build/changelogger/src/render.rs

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use smithy_rs_tool_common::changelog::{
1414
ValidationSet,
1515
};
1616
use smithy_rs_tool_common::git::{find_git_repository_root, Git, GitCLI};
17+
use smithy_rs_tool_common::release_tag::ReleaseTag;
1718
use smithy_rs_tool_common::versions_manifest::{CrateVersionMetadataMap, VersionsManifest};
1819
use std::env;
1920
use std::fmt::Write;
@@ -80,6 +81,9 @@ pub struct RenderArgs {
8081
// working directory will be used to attempt to find it.
8182
#[clap(long, action)]
8283
pub smithy_rs_location: Option<PathBuf>,
84+
// Location of the aws-sdk-rust repository, used exclusively to retrieve existing release tags.
85+
#[clap(long, required_if_eq("change-set", "aws-sdk"))]
86+
pub aws_sdk_rust_location: Option<PathBuf>,
8387

8488
// For testing only
8589
#[clap(skip)]
@@ -97,18 +101,76 @@ pub fn subcommand_render(args: &RenderArgs) -> Result<()> {
97101
.unwrap_or(current_dir.as_path()),
98102
)
99103
.context("failed to find smithy-rs repo root")?;
100-
let smithy_rs = GitCLI::new(&repo_root)?;
101104

105+
let current_tag = {
106+
let cli_for_tag = if let Some(aws_sdk_rust_repo_root) = &args.aws_sdk_rust_location {
107+
GitCLI::new(
108+
&find_git_repository_root("aws-sdk-rust", aws_sdk_rust_repo_root)
109+
.context("failed to find aws-sdk-rust repo root")?,
110+
)?
111+
} else {
112+
GitCLI::new(&repo_root)?
113+
};
114+
cli_for_tag.get_current_tag()?
115+
};
116+
let next_release_tag = next_tag(now, &current_tag);
117+
118+
let smithy_rs = GitCLI::new(&repo_root)?;
102119
if args.independent_versioning {
103-
let smithy_rs_metadata =
104-
date_based_release_metadata(now, "smithy-rs-release-manifest.json");
105-
let sdk_metadata = date_based_release_metadata(now, "aws-sdk-rust-release-manifest.json");
120+
let smithy_rs_metadata = date_based_release_metadata(
121+
now,
122+
next_release_tag.clone(),
123+
"smithy-rs-release-manifest.json",
124+
);
125+
let sdk_metadata = date_based_release_metadata(
126+
now,
127+
next_release_tag,
128+
"aws-sdk-rust-release-manifest.json",
129+
);
106130
update_changelogs(args, &smithy_rs, &smithy_rs_metadata, &sdk_metadata)
107131
} else {
108132
bail!("the --independent-versioning flag must be set; synchronized versioning no longer supported");
109133
}
110134
}
111135

136+
// Generate a unique date-based release tag
137+
//
138+
// This function generates a date-based release tag and compares it to `current_tag`.
139+
// If the generated tag is a substring of `current_tag`, it indicates that a release has already occurred on that day.
140+
// In this case, the function ensures uniqueness by appending a numerical suffix to `current_tag`.
141+
fn next_tag(now: OffsetDateTime, current_tag: &ReleaseTag) -> String {
142+
let date_based_release_tag = format!(
143+
"release-{year}-{month:02}-{day:02}",
144+
year = now.date().year(),
145+
month = u8::from(now.date().month()),
146+
day = now.date().day()
147+
);
148+
149+
let current_tag = current_tag.as_str();
150+
if current_tag.starts_with(&date_based_release_tag) {
151+
bump_release_tag_suffix(current_tag)
152+
} else {
153+
date_based_release_tag
154+
}
155+
}
156+
157+
// Bump `current_tag` by adding or incrementing a numerical suffix
158+
//
159+
// This is a private function that is only called by `next_tag`.
160+
// It assumes that `current_tag` follows the format `release-YYYY-MM-DD`.
161+
fn bump_release_tag_suffix(current_tag: &str) -> String {
162+
if let Some(pos) = current_tag.rfind('.') {
163+
let prefix = &current_tag[..pos];
164+
let suffix = &current_tag[pos + 1..];
165+
let suffix = suffix
166+
.parse::<u32>()
167+
.expect("should parse numerical suffix");
168+
format!("{}.{}", prefix, suffix + 1)
169+
} else {
170+
format!("{}.{}", current_tag, 2)
171+
}
172+
}
173+
112174
struct ReleaseMetadata {
113175
title: String,
114176
tag: String,
@@ -126,16 +188,12 @@ struct ReleaseManifest {
126188

127189
fn date_based_release_metadata(
128190
now: OffsetDateTime,
191+
tag: String,
129192
manifest_name: impl Into<String>,
130193
) -> ReleaseMetadata {
131194
ReleaseMetadata {
132195
title: date_title(&now),
133-
tag: format!(
134-
"release-{year}-{month:02}-{day:02}",
135-
year = now.date().year(),
136-
month = u8::from(now.date().month()),
137-
day = now.date().day()
138-
),
196+
tag,
139197
manifest_name: manifest_name.into(),
140198
}
141199
}
@@ -506,14 +564,19 @@ pub(crate) fn render(
506564

507565
#[cfg(test)]
508566
mod test {
509-
use super::{date_based_release_metadata, render, Changelog, ChangelogEntries, ChangelogEntry};
567+
use super::{
568+
bump_release_tag_suffix, date_based_release_metadata, next_tag, render, Changelog,
569+
ChangelogEntries, ChangelogEntry,
570+
};
510571
use smithy_rs_tool_common::changelog::ChangelogLoader;
572+
use smithy_rs_tool_common::release_tag::ReleaseTag;
511573
use smithy_rs_tool_common::{
512574
changelog::SdkAffected,
513575
package::PackageCategory,
514576
versions_manifest::{CrateVersion, CrateVersionMetadataMap},
515577
};
516578
use std::fs;
579+
use std::str::FromStr;
517580
use tempfile::TempDir;
518581
use time::OffsetDateTime;
519582

@@ -662,7 +725,8 @@ message = "Some API change"
662725
#[test]
663726
fn test_date_based_release_metadata() {
664727
let now = OffsetDateTime::from_unix_timestamp(100_000_000).unwrap();
665-
let result = date_based_release_metadata(now, "some-manifest.json");
728+
let result =
729+
date_based_release_metadata(now, "release-1973-03-03".to_owned(), "some-manifest.json");
666730
assert_eq!("March 3rd, 1973", result.title);
667731
assert_eq!("release-1973-03-03", result.tag);
668732
assert_eq!("some-manifest.json", result.manifest_name);
@@ -817,4 +881,40 @@ message = "Some new API to do X"
817881
.trim_start();
818882
pretty_assertions::assert_str_eq!(release_notes, expected_body);
819883
}
884+
885+
#[test]
886+
fn test_bump_release_tag_suffix() {
887+
for (expected, input) in &[
888+
("release-2024-07-18.2", "release-2024-07-18"),
889+
("release-2024-07-18.3", "release-2024-07-18.2"),
890+
(
891+
"release-2024-07-18.4294967295", // u32::MAX
892+
"release-2024-07-18.4294967294",
893+
),
894+
] {
895+
assert_eq!(*expected, &bump_release_tag_suffix(*input));
896+
}
897+
}
898+
899+
#[test]
900+
fn test_next_tag() {
901+
// `now` falls on 2024-10-14
902+
let now = OffsetDateTime::from_unix_timestamp(1_728_938_598).unwrap();
903+
assert_eq!(
904+
"release-2024-10-14",
905+
&next_tag(now, &ReleaseTag::from_str("release-2024-10-13").unwrap()),
906+
);
907+
assert_eq!(
908+
"release-2024-10-14.2",
909+
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14").unwrap()),
910+
);
911+
assert_eq!(
912+
"release-2024-10-14.3",
913+
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14.2").unwrap()),
914+
);
915+
assert_eq!(
916+
"release-2024-10-14.10",
917+
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14.9").unwrap()),
918+
);
919+
}
820920
}

0 commit comments

Comments
 (0)