Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,12 @@ the matching upload and download steps together.
- `.release/release-bundles.json`

`publish-release` defaults to `.release/release-bundles.json` as its asset
manifest and uploads only the listed `.tar.zst` files from `.release/bundles`.
manifest and uploads the listed `.tar.zst` files from `.release/bundles`.
Use its `additional_assets` input for newline-separated paths to other release
files, such as documentation archives. All bundle and additional assets are
passed to the same `gh release create` invocation so they are attached before
the release is published. This is required when immutable releases are enabled;
assets cannot be uploaded to an immutable release after publication.

For release candidates such as `1.2.3-rc1`, `resolve-previous-release` still
uses GitHub's latest stable release as the default previous bundle. Set
Expand Down
5 changes: 5 additions & 0 deletions actions/publish-release/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ inputs:
release_list_file:
description: File listing release bundle assets produced by prepare-bundles.
default: .release/release-bundles.json
additional_assets:
description: Newline-separated paths to additional files to attach before publishing the release.
default: ""
repo:
description: GitHub repository in owner/name form. Empty uses GITHUB_REPOSITORY.
default: ""
Expand All @@ -35,6 +38,7 @@ runs:
RELEASE_NOTES_FILE: ${{ inputs.release_notes_file }}
BUNDLE_DIR: ${{ inputs.bundle_dir }}
RELEASE_LIST_FILE: ${{ inputs.release_list_file }}
ADDITIONAL_ASSETS_INPUT: ${{ inputs.additional_assets }}
REPO_INPUT: ${{ inputs.repo }}
TARGET_INPUT: ${{ inputs.target }}
CHECK_AVAILABILITY: ${{ inputs.check_availability }}
Expand All @@ -51,4 +55,5 @@ runs:
--notes-file "$RELEASE_NOTES_FILE" \
--bundle-dir "$BUNDLE_DIR" \
--release-list-file "$RELEASE_LIST_FILE" \
--additional-assets "$ADDITIONAL_ASSETS_INPUT" \
--check-availability "$CHECK_AVAILABILITY"
25 changes: 25 additions & 0 deletions scripts/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def build_parser() -> argparse.ArgumentParser:
publish.add_argument("--notes-file", required=True)
publish.add_argument("--bundle-dir", required=True)
publish.add_argument("--release-list-file", required=True)
publish.add_argument("--additional-assets", default="")
publish.add_argument("--check-availability", choices=["true", "false"], default="true")
publish.set_defaults(func=cmd_publish_release)

Expand Down Expand Up @@ -494,6 +495,8 @@ def cmd_publish_release(args: argparse.Namespace) -> int:
target = require_target(env_fallback(args.target, "GITHUB_SHA"))
notes_file = require_nonempty_file(args.notes_file, "release notes file")
assets = release_assets(args.bundle_dir, args.release_list_file)
assets.extend(additional_release_assets(args.additional_assets))
require_unique_release_asset_names(assets)

if args.check_availability == "true":
check_availability(version, repo)
Expand Down Expand Up @@ -1147,6 +1150,28 @@ def release_assets(bundle_dir_text: str | Path, release_list_file_text: str | Pa
return assets


def additional_release_assets(value: str) -> list[Path]:
assets: list[Path] = []
for line in value.splitlines():
path_text = line.strip()
if not path_text:
continue
if "#" in path_text:
raise ReleaseError(
f"additional release asset path must not contain '#': {path_text}"
)
assets.append(require_nonempty_file(path_text, "additional release asset"))
return assets


def require_unique_release_asset_names(assets: list[Path]) -> None:
seen: set[str] = set()
for asset in assets:
if asset.name in seen:
raise ReleaseError(f"duplicate release asset filename: {asset.name}")
seen.add(asset.name)


def run(command: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/composite-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:
- run: |
mkdir -p .release/bundles
printf 'bundle' > .release/bundles/pkg.tar.zst
printf 'docs' > docs.tar.gz
printf '[{"name":"default","artifact_file":"pkg.tar.zst","source_path":"dist/pkg.tar.zst"}]\n' > .release/release-bundles.json

- uses: ./actions/make-release-notes
Expand All @@ -84,6 +85,7 @@ jobs:
- uses: ./actions/publish-release
with:
release_version: ${{ needs.build.outputs.release_version }}
additional_assets: docs.tar.gz
check_availability: "false"

docs:
Expand Down
62 changes: 61 additions & 1 deletion tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,15 @@ def test_create_followup_pr_rejects_invalid_branch_prefix(self):

# --- publish-release ---

def publish_args(self, version="1.2.3", notes=None, bundle_dir=None, release_list=None, check_availability="true"):
def publish_args(
self,
version="1.2.3",
notes=None,
bundle_dir=None,
release_list=None,
additional_assets="",
check_availability="true",
):
return parse_args(
"publish-release",
"--version", version,
Expand All @@ -786,6 +794,7 @@ def publish_args(self, version="1.2.3", notes=None, bundle_dir=None, release_lis
"--notes-file", str(notes),
"--bundle-dir", str(bundle_dir),
"--release-list-file", str(release_list),
"--additional-assets", additional_assets,
"--check-availability", check_availability,
)

Expand Down Expand Up @@ -835,6 +844,57 @@ def test_publish_release_rechecks_availability_and_creates_release(self):
self.assertNotIn("debug.txt", commands)
self.assertIn("--notes-file", commands)

def test_publish_release_includes_additional_assets(self):
log = self.tmp / "commands.log"
notes = self.write("release-notes.md", "Generated notes\n")
bundle_dir = self.tmp / "bundles"
bundle_dir.mkdir()
self.write("bundles/pkg.tar.zst", "bundle")
docs = self.write("docs.tar.gz", "docs")
release_list = self.write(
"release-bundles.json",
json.dumps([{"name": "default", "artifact_file": "pkg.tar.zst"}]),
)
with self.fake_commands(
env={"LOG_PATH": str(log)},
git='echo "git:$*" >> "$LOG_PATH"\n',
gh='echo "gh:$*" >> "$LOG_PATH"\n',
):
release.cmd_publish_release(
self.publish_args(
notes=notes,
bundle_dir=bundle_dir,
release_list=release_list,
additional_assets=f"\n{docs}\n",
check_availability="false",
)
)
commands = log.read_text(encoding="utf-8")
self.assertIn(f"gh:release create 1.2.3 {bundle_dir / 'pkg.tar.zst'} {docs}", commands)

def test_publish_release_rejects_duplicate_additional_asset_filename(self):
notes = self.write("release-notes.md", "Generated notes\n")
bundle_dir = self.tmp / "bundles"
bundle_dir.mkdir()
self.write("bundles/pkg.tar.zst", "bundle")
additional_dir = self.tmp / "additional"
additional_dir.mkdir()
duplicate = self.write("additional/pkg.tar.zst", "duplicate")
release_list = self.write(
"release-bundles.json",
json.dumps([{"name": "default", "artifact_file": "pkg.tar.zst"}]),
)
with self.assertRaisesRegex(release.ReleaseError, "duplicate release asset filename"):
release.cmd_publish_release(
self.publish_args(
notes=notes,
bundle_dir=bundle_dir,
release_list=release_list,
additional_assets=str(duplicate),
check_availability="false",
)
)

def test_publish_release_marks_prerelease(self):
log = self.tmp / "commands.log"
notes = self.write("release-notes.md", "Generated notes\n")
Expand Down