Skip to content

Commit

Permalink
Allow multiple releases per day in the smithy-rs repository (#3875)
Browse files Browse the repository at this point in the history
## 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._
  • Loading branch information
ysaito1001 authored Oct 18, 2024
1 parent c8c610f commit de4bc45
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 273 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
with:
path: smithy-rs
ref: ${{ inputs.git_ref }}
# `generate-smithy-rs-release` requires access to previous tags to determine if a numerical suffix is needed
# to make the release tag unique
fetch-depth: 0
# The models from aws-sdk-rust are needed to generate the full SDK for CI
- uses: actions/checkout@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-scripts/create-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const assert = require("assert");
const fs = require("fs");

const smithy_rs_repo = {
owner: "awslabs",
owner: "smithy-lang",
repo: "smithy-rs",
};

Expand Down
37 changes: 24 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,18 @@ jobs:
ref: ${{ inputs.commit_sha }}
path: smithy-rs
token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
fetch-depth: 0
- name: Generate release artifacts
uses: ./smithy-rs/.github/actions/docker-build
with:
action: generate-smithy-rs-release
- name: Download all artifacts
uses: ./smithy-rs/.github/actions/download-all-artifacts
# This step is not idempotent, as it pushes release artifacts to the `smithy-rs-release-1.x.y` branch. However,
# if this step succeeds but a subsequent step fails, retrying the release workflow is "safe" in that it does not
# create any inconsistent states; this step would simply fail because the release branch would be ahead of `main`
# due to previously pushed artifacts.
# To successfully retry a release, revert the commits in the release branch that pushed the artifacts.
- name: Push smithy-rs changes
shell: bash
working-directory: smithy-rs-release/smithy-rs
Expand All @@ -202,7 +208,7 @@ jobs:
# to retry a release action execution that failed due to a transient issue.
# In that case, we expect the commit to be releasable as-is, i.e. the changelog should have already
# been processed.
git fetch --unshallow
git fetch
if [[ "${DRY_RUN}" == "true" ]]; then
# During dry-runs, "git push" without "--force" can fail if smithy-rs-release-x.y.z-preview is behind
# smithy-rs-release-x.y.z, but that does not matter much during dry-runs.
Expand All @@ -214,18 +220,7 @@ jobs:
fi
fi
echo "commit_sha=$(git rev-parse HEAD)" > $GITHUB_OUTPUT
- name: Tag release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
script: |
const createReleaseScript = require("./smithy-rs/.github/workflows/release-scripts/create-release.js");
await createReleaseScript({
github,
isDryRun: ${{ inputs.dry_run }},
releaseManifestPath: "smithy-rs-release/smithy-rs-release-manifest.json",
releaseCommitish: "${{ steps.push-changelog.outputs.commit_sha }}"
});
# This step is idempotent; the `publisher` will not publish a crate if the version is already published on crates.io.
- name: Publish to crates.io
shell: bash
working-directory: smithy-rs-release/crates-to-publish
Expand All @@ -247,7 +242,23 @@ jobs:
else
publisher publish -y --location .
fi
# This step is not idempotent and MUST be performed last, as it will generate a new release in the `smithy-rs`
# repository with the release tag that is always unique and has an increasing numerical suffix.
- name: Tag release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RELEASE_AUTOMATION_BOT_PAT }}
script: |
const createReleaseScript = require("./smithy-rs/.github/workflows/release-scripts/create-release.js");
await createReleaseScript({
github,
isDryRun: ${{ inputs.dry_run }},
releaseManifestPath: "smithy-rs-release/smithy-rs-release-manifest.json",
releaseCommitish: "${{ steps.push-changelog.outputs.commit_sha }}"
});
# If this step fails for any reason, there's no need to retry the release workflow, as this step is auxiliary
# and the release itself was successful. Instead, manually trigger `backport-pull-request.yml`.
open-backport-pull-request:
name: Open backport pull request to merge the release branch back to main
needs:
Expand Down
2 changes: 1 addition & 1 deletion tools/ci-build/changelogger/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tools/ci-build/changelogger/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "changelogger"
version = "0.2.0"
version = "0.3.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "A CLI tool render and update changelogs from changelog files"
edition = "2021"
Expand Down
14 changes: 12 additions & 2 deletions tools/ci-build/changelogger/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mod tests {
previous_release_versions_manifest: None,
date_override: None,
smithy_rs_location: None,
aws_sdk_rust_location: None,
})
},
Args::try_parse_from([
Expand Down Expand Up @@ -124,6 +125,7 @@ mod tests {
previous_release_versions_manifest: None,
date_override: None,
smithy_rs_location: None,
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
})
},
Args::try_parse_from([
Expand All @@ -140,6 +142,8 @@ mod tests {
"fromplace",
"--changelog-output",
"some-changelog",
"--aws-sdk-rust-location",
"aws-sdk-rust-location",
])
.unwrap()
);
Expand All @@ -159,6 +163,7 @@ mod tests {
)),
date_override: None,
smithy_rs_location: None,
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
})
},
Args::try_parse_from([
Expand All @@ -174,7 +179,9 @@ mod tests {
"--changelog-output",
"some-changelog",
"--previous-release-versions-manifest",
"path/to/versions.toml"
"path/to/versions.toml",
"--aws-sdk-rust-location",
"aws-sdk-rust-location",
])
.unwrap()
);
Expand All @@ -196,6 +203,7 @@ mod tests {
)),
date_override: None,
smithy_rs_location: None,
aws_sdk_rust_location: Some(PathBuf::from("aws-sdk-rust-location")),
})
},
Args::try_parse_from([
Expand All @@ -213,7 +221,9 @@ mod tests {
"--current-release-versions-manifest",
"path/to/current/versions.toml",
"--previous-release-versions-manifest",
"path/to/previous/versions.toml"
"path/to/previous/versions.toml",
"--aws-sdk-rust-location",
"aws-sdk-rust-location",
])
.unwrap()
);
Expand Down
124 changes: 112 additions & 12 deletions tools/ci-build/changelogger/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use smithy_rs_tool_common::changelog::{
ValidationSet,
};
use smithy_rs_tool_common::git::{find_git_repository_root, Git, GitCLI};
use smithy_rs_tool_common::release_tag::ReleaseTag;
use smithy_rs_tool_common::versions_manifest::{CrateVersionMetadataMap, VersionsManifest};
use std::env;
use std::fmt::Write;
Expand Down Expand Up @@ -80,6 +81,9 @@ pub struct RenderArgs {
// working directory will be used to attempt to find it.
#[clap(long, action)]
pub smithy_rs_location: Option<PathBuf>,
// Location of the aws-sdk-rust repository, used exclusively to retrieve existing release tags.
#[clap(long, required_if_eq("change-set", "aws-sdk"))]
pub aws_sdk_rust_location: Option<PathBuf>,

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

let current_tag = {
let cli_for_tag = if let Some(aws_sdk_rust_repo_root) = &args.aws_sdk_rust_location {
GitCLI::new(
&find_git_repository_root("aws-sdk-rust", aws_sdk_rust_repo_root)
.context("failed to find aws-sdk-rust repo root")?,
)?
} else {
GitCLI::new(&repo_root)?
};
cli_for_tag.get_current_tag()?
};
let next_release_tag = next_tag(now, &current_tag);

let smithy_rs = GitCLI::new(&repo_root)?;
if args.independent_versioning {
let smithy_rs_metadata =
date_based_release_metadata(now, "smithy-rs-release-manifest.json");
let sdk_metadata = date_based_release_metadata(now, "aws-sdk-rust-release-manifest.json");
let smithy_rs_metadata = date_based_release_metadata(
now,
next_release_tag.clone(),
"smithy-rs-release-manifest.json",
);
let sdk_metadata = date_based_release_metadata(
now,
next_release_tag,
"aws-sdk-rust-release-manifest.json",
);
update_changelogs(args, &smithy_rs, &smithy_rs_metadata, &sdk_metadata)
} else {
bail!("the --independent-versioning flag must be set; synchronized versioning no longer supported");
}
}

// Generate a unique date-based release tag
//
// This function generates a date-based release tag and compares it to `current_tag`.
// If the generated tag is a substring of `current_tag`, it indicates that a release has already occurred on that day.
// In this case, the function ensures uniqueness by appending a numerical suffix to `current_tag`.
fn next_tag(now: OffsetDateTime, current_tag: &ReleaseTag) -> String {
let date_based_release_tag = format!(
"release-{year}-{month:02}-{day:02}",
year = now.date().year(),
month = u8::from(now.date().month()),
day = now.date().day()
);

let current_tag = current_tag.as_str();
if current_tag.starts_with(&date_based_release_tag) {
bump_release_tag_suffix(current_tag)
} else {
date_based_release_tag
}
}

// Bump `current_tag` by adding or incrementing a numerical suffix
//
// This is a private function that is only called by `next_tag`.
// It assumes that `current_tag` follows the format `release-YYYY-MM-DD`.
fn bump_release_tag_suffix(current_tag: &str) -> String {
if let Some(pos) = current_tag.rfind('.') {
let prefix = &current_tag[..pos];
let suffix = &current_tag[pos + 1..];
let suffix = suffix
.parse::<u32>()
.expect("should parse numerical suffix");
format!("{}.{}", prefix, suffix + 1)
} else {
format!("{}.{}", current_tag, 2)
}
}

struct ReleaseMetadata {
title: String,
tag: String,
Expand All @@ -126,16 +188,12 @@ struct ReleaseManifest {

fn date_based_release_metadata(
now: OffsetDateTime,
tag: String,
manifest_name: impl Into<String>,
) -> ReleaseMetadata {
ReleaseMetadata {
title: date_title(&now),
tag: format!(
"release-{year}-{month:02}-{day:02}",
year = now.date().year(),
month = u8::from(now.date().month()),
day = now.date().day()
),
tag,
manifest_name: manifest_name.into(),
}
}
Expand Down Expand Up @@ -506,14 +564,19 @@ pub(crate) fn render(

#[cfg(test)]
mod test {
use super::{date_based_release_metadata, render, Changelog, ChangelogEntries, ChangelogEntry};
use super::{
bump_release_tag_suffix, date_based_release_metadata, next_tag, render, Changelog,
ChangelogEntries, ChangelogEntry,
};
use smithy_rs_tool_common::changelog::ChangelogLoader;
use smithy_rs_tool_common::release_tag::ReleaseTag;
use smithy_rs_tool_common::{
changelog::SdkAffected,
package::PackageCategory,
versions_manifest::{CrateVersion, CrateVersionMetadataMap},
};
use std::fs;
use std::str::FromStr;
use tempfile::TempDir;
use time::OffsetDateTime;

Expand Down Expand Up @@ -662,7 +725,8 @@ message = "Some API change"
#[test]
fn test_date_based_release_metadata() {
let now = OffsetDateTime::from_unix_timestamp(100_000_000).unwrap();
let result = date_based_release_metadata(now, "some-manifest.json");
let result =
date_based_release_metadata(now, "release-1973-03-03".to_owned(), "some-manifest.json");
assert_eq!("March 3rd, 1973", result.title);
assert_eq!("release-1973-03-03", result.tag);
assert_eq!("some-manifest.json", result.manifest_name);
Expand Down Expand Up @@ -817,4 +881,40 @@ message = "Some new API to do X"
.trim_start();
pretty_assertions::assert_str_eq!(release_notes, expected_body);
}

#[test]
fn test_bump_release_tag_suffix() {
for (expected, input) in &[
("release-2024-07-18.2", "release-2024-07-18"),
("release-2024-07-18.3", "release-2024-07-18.2"),
(
"release-2024-07-18.4294967295", // u32::MAX
"release-2024-07-18.4294967294",
),
] {
assert_eq!(*expected, &bump_release_tag_suffix(*input));
}
}

#[test]
fn test_next_tag() {
// `now` falls on 2024-10-14
let now = OffsetDateTime::from_unix_timestamp(1_728_938_598).unwrap();
assert_eq!(
"release-2024-10-14",
&next_tag(now, &ReleaseTag::from_str("release-2024-10-13").unwrap()),
);
assert_eq!(
"release-2024-10-14.2",
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14").unwrap()),
);
assert_eq!(
"release-2024-10-14.3",
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14.2").unwrap()),
);
assert_eq!(
"release-2024-10-14.10",
&next_tag(now, &ReleaseTag::from_str("release-2024-10-14.9").unwrap()),
);
}
}
Loading

0 comments on commit de4bc45

Please sign in to comment.