mirror of
https://github.com/softprops/action-gh-release.git
synced 2026-03-15 09:20:54 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f917660b | ||
|
|
1f3f350167 | ||
|
|
37819cb191 | ||
|
|
9312864490 |
80
AGENTS.md
Normal file
80
AGENTS.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# action-gh-release
|
||||
|
||||
This repository is maintained as a small, user-facing GitHub Action with a relatively wide compatibility surface.
|
||||
Optimize for stability, reproducibility, and clear user value over broad rewrites.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Prefer narrow behavior fixes over structural churn.
|
||||
- Reproduce current behavior on `master` before changing code.
|
||||
- Treat GitHub platform behavior as distinct from action behavior.
|
||||
If GitHub controls the outcome, prefer docs or clearer errors over brittle workarounds.
|
||||
- Do not revive stale PRs mechanically.
|
||||
Reuse the idea if it still has value, but reimplement on top of current `master`.
|
||||
- Avoid standalone refactors with no clear user-facing benefit.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
- `src/main.ts` is the orchestration layer: parse config, validate inputs, create/update release, upload assets, finalize, set outputs.
|
||||
- `src/github.ts` owns release semantics: lookup, create/update/finalize, asset upload, race handling, and GitHub API interaction.
|
||||
- `src/util.ts` owns parsing and path normalization.
|
||||
- Keep behavior-specific logic in `src/github.ts` or `src/util.ts`; avoid growing `src/main.ts` with ad-hoc feature branches.
|
||||
|
||||
## Bug-Fix Workflow
|
||||
|
||||
- Reproduce the issue against current `master` first.
|
||||
- When available, use the companion consumer harness repo `action-gh-release-test`.
|
||||
- Capture exact workflow run URLs and release URLs before claiming a fix.
|
||||
- If the issue is really a docs/usage or platform-limit case, document it and close it as such instead of forcing a code change.
|
||||
- If a historical issue no longer reproduces on current `master`, prefer a short closeout note that asks the reporter to open a fresh issue if they still see it.
|
||||
|
||||
## Feature Triage
|
||||
|
||||
- Ship features only when there is clear user value or repeated demand.
|
||||
- Small convenience features are fine, but they should stay small.
|
||||
- Weak-demand features should not expand parsing complexity, cross-platform ambiguity, or maintenance surface.
|
||||
- For old feature PRs:
|
||||
- check whether current `master` already covers the behavior
|
||||
- prefer a tiny docs clarification if the behavior exists but is poorly explained
|
||||
- close stale feature PRs when the idea is obsolete, low-value, or badly shaped for the current codebase
|
||||
|
||||
## Contract Sync
|
||||
|
||||
When behavior changes, keep the external contract in sync:
|
||||
|
||||
- update `README.md`
|
||||
- update `action.yml`
|
||||
- update tests under `__tests__/`
|
||||
- regenerate `dist/index.js` with `npm run build`
|
||||
|
||||
Docs-only changes do not need `dist/index.js` regeneration.
|
||||
|
||||
## Verification
|
||||
|
||||
For code changes, run:
|
||||
|
||||
- `npm run fmtcheck`
|
||||
- `npm run typecheck`
|
||||
- `npm run build`
|
||||
- `npm test`
|
||||
|
||||
For behavior changes, also run the relevant external regression workflow(s) in `action-gh-release-test` against the exact ref under test.
|
||||
|
||||
## Release and Triage Conventions
|
||||
|
||||
- Keep PR labels accurate. Release notes depend on them.
|
||||
- bug fixes: `bug`
|
||||
- docs-only changes: `documentation`
|
||||
- additive features: `feature` or `enhancement`
|
||||
- dependency updates: `dependencies`
|
||||
- Follow [RELEASE.md](RELEASE.md) for version bumps, changelog updates, tagging, and release publication.
|
||||
- Prefer manual issue/PR closeouts with a short rationale over implicit assumptions.
|
||||
- Do not auto-close old PRs or issues through unrelated docs PRs.
|
||||
|
||||
## Implementation Preferences
|
||||
|
||||
- Preserve the current upload/finalize flow unless there is strong evidence it needs to change.
|
||||
- Prefer upload-time semantics over filesystem mutation.
|
||||
- Be careful with parsing changes around `files`, path handling, and Windows compatibility.
|
||||
- Be careful with race-condition fixes; verify both local tests and consumer-repo concurrency harnesses.
|
||||
- Do not assume a refactor is safe just because tests are green. This action’s behavior is heavily shaped by GitHub API edge cases.
|
||||
27
README.md
27
README.md
@@ -173,6 +173,20 @@ jobs:
|
||||
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
When you use GitHub's built-in `generate_release_notes` support, you can optionally
|
||||
pin the comparison base explicitly with `previous_tag`. This is useful when the default
|
||||
comparison range does not match the release series you want to publish.
|
||||
|
||||
```yaml
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: stage-2026-03-15
|
||||
target_commitish: ${{ github.sha }}
|
||||
previous_tag: prod-2026-03-01
|
||||
generate_release_notes: true
|
||||
```
|
||||
|
||||
### 💅 Customizing
|
||||
|
||||
#### inputs
|
||||
@@ -183,7 +197,7 @@ The following are optional as `step.with` keys
|
||||
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `body` | String | Text communicating notable changes in this release |
|
||||
| `body_path` | String | Path to load text communicating notable changes in this release |
|
||||
| `draft` | Boolean | Indicator of whether or not this release is a draft |
|
||||
| `draft` | Boolean | Keep the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload. |
|
||||
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
|
||||
| `preserve_order` | Boolean | Upload assets sequentially in the provided order. This controls the action's upload behavior, but it does not control the final asset ordering that GitHub may display on the release page or return from the Releases API. |
|
||||
| `files` | String | Newline-delimited globs of paths to assets to upload for release. Escape glob metacharacters when you need to match a literal filename that contains them, such as `[` or `]`. `~/...` expands to the runner home directory. On Windows, both `\` and `/` separators are accepted. GitHub may normalize raw asset filenames that contain special characters; the action restores the asset label when possible, but the final download name remains GitHub-controlled. |
|
||||
@@ -196,15 +210,20 @@ The following are optional as `step.with` keys
|
||||
| `token` | String | Authorized GitHub token or PAT. Defaults to `${{ github.token }}` when omitted. A non-empty explicit token overrides `GITHUB_TOKEN`. Passing `""` treats the token as explicitly unset, so omit the input entirely or use an expression such as `${{ inputs.token || github.token }}` when wrapping this action in a composite action. |
|
||||
| `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
|
||||
| `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
|
||||
| `previous_tag` | String | Optional. When `generate_release_notes` is enabled, use this tag as GitHub's `previous_tag_name` comparison base. If omitted, GitHub chooses the comparison base automatically. |
|
||||
| `append_body` | Boolean | Append to existing body instead of overwriting it |
|
||||
| `make_latest` | String | Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api defaults if not provided |
|
||||
|
||||
💡 When providing a `body` and `body_path` at the same time, `body_path` will be
|
||||
attempted first, then falling back on `body` if the path can not be read from.
|
||||
|
||||
💡 When the release info keys (such as `name`, `body`, `draft`, `prerelease`, etc.)
|
||||
are not explicitly set and there is already an existing release for the tag, the
|
||||
release will retain its original info.
|
||||
💡 When the release info keys (such as `name`, `body`, `prerelease`, etc.) are not
|
||||
explicitly set and there is already an existing release for the tag, the release
|
||||
will retain its original info.
|
||||
|
||||
💡 Draft status is handled separately during finalization. If the action reuses an
|
||||
existing draft release, set `draft: true` to keep it draft; if `draft` is omitted,
|
||||
the action will publish that draft after uploading assets.
|
||||
|
||||
💡 `files` is glob-based, so literal filenames that contain glob metacharacters such as
|
||||
`[` or `]` must be escaped in the pattern.
|
||||
|
||||
41
RELEASE.md
Normal file
41
RELEASE.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Release Workflow
|
||||
|
||||
Use this checklist when cutting a new `action-gh-release` release.
|
||||
|
||||
## Inputs
|
||||
|
||||
- Decide the semantic version bump first: `major`, `minor`, or `patch`.
|
||||
- Review recent merged PRs and labels before drafting the changelog entry.
|
||||
- Make sure `master` is current and the worktree is clean before starting.
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Update [package.json](package.json) to the new version.
|
||||
2. Add the new entry at the top of [CHANGELOG.md](CHANGELOG.md).
|
||||
- Summarize the release in 1 short paragraph.
|
||||
- Prefer user-facing fixes and features over internal churn.
|
||||
- Keep the merged PR list aligned with `.github/release.yml` categories.
|
||||
3. Run `npm i` to refresh [package-lock.json](package-lock.json).
|
||||
4. Run the full local verification set:
|
||||
- `npm run fmtcheck`
|
||||
- `npm run typecheck`
|
||||
- `npm run build`
|
||||
- `npm test`
|
||||
5. Commit the release prep.
|
||||
- Use a plain release commit message like `release 2.5.4`.
|
||||
6. Create the annotated tag for the release commit.
|
||||
- Example: `git tag -a v2.5.4 -m "v2.5.4"`
|
||||
7. Push the commit and tag.
|
||||
- Example: `git push origin master && git push origin v2.5.4`
|
||||
8. Move the floating major tag to the new release tag.
|
||||
- For the current major line, either run `npm run updatetag` or update the script first if the major ever changes.
|
||||
- Verify the floating tag points at the same commit as the new full tag.
|
||||
9. Create the GitHub release from the new tag.
|
||||
- Prefer the release body from [CHANGELOG.md](CHANGELOG.md), then let GitHub append generated notes only if they add value.
|
||||
- Verify the release shows the expected tag, title, notes, and attached artifacts.
|
||||
|
||||
## Notes
|
||||
|
||||
- Behavior changes should already have matching updates in [README.md](README.md), [action.yml](action.yml), tests, and `dist/index.js` before release prep begins.
|
||||
- Docs-only releases still need an intentional changelog entry and version bump decision.
|
||||
- If a release is mainly bug fixes, keep the title and summary patch-oriented; do not bury the actual fixes under dependency noise.
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
asset,
|
||||
findTagFromReleases,
|
||||
finalizeRelease,
|
||||
GitHubReleaser,
|
||||
mimeOrDefault,
|
||||
release,
|
||||
Release,
|
||||
@@ -32,6 +33,7 @@ describe('github', () => {
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_previous_tag: undefined,
|
||||
input_append_body: false,
|
||||
input_make_latest: undefined,
|
||||
};
|
||||
@@ -146,6 +148,86 @@ describe('github', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubReleaser', () => {
|
||||
it('passes previous_tag_name to generateReleaseNotes and strips it from createRelease', async () => {
|
||||
const generateReleaseNotes = vi.fn(async () => ({
|
||||
data: {
|
||||
name: 'Generated release',
|
||||
body: "## What's Changed\n* Added support for previous_tag",
|
||||
},
|
||||
}));
|
||||
const createRelease = vi.fn(async (params) => ({
|
||||
data: {
|
||||
id: 1,
|
||||
upload_url: 'test',
|
||||
html_url: 'test',
|
||||
tag_name: params.tag_name,
|
||||
name: params.name,
|
||||
body: params.body,
|
||||
target_commitish: params.target_commitish || 'main',
|
||||
draft: params.draft ?? false,
|
||||
prerelease: params.prerelease ?? false,
|
||||
assets: [],
|
||||
},
|
||||
}));
|
||||
|
||||
const releaser = new GitHubReleaser({
|
||||
rest: {
|
||||
repos: {
|
||||
generateReleaseNotes,
|
||||
createRelease,
|
||||
updateRelease: vi.fn(),
|
||||
getReleaseByTag: vi.fn(),
|
||||
listReleaseAssets: vi.fn(),
|
||||
deleteReleaseAsset: vi.fn(),
|
||||
deleteRelease: vi.fn(),
|
||||
updateReleaseAsset: vi.fn(),
|
||||
listReleases: {
|
||||
endpoint: {
|
||||
merge: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paginate: {
|
||||
iterator: vi.fn(),
|
||||
},
|
||||
request: vi.fn(),
|
||||
} as any);
|
||||
|
||||
await releaser.createRelease({
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
tag_name: 'v1.0.0',
|
||||
name: 'v1.0.0',
|
||||
body: 'Intro',
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
target_commitish: 'abc123',
|
||||
discussion_category_name: undefined,
|
||||
generate_release_notes: true,
|
||||
make_latest: undefined,
|
||||
previous_tag_name: 'v0.9.0',
|
||||
});
|
||||
|
||||
expect(generateReleaseNotes).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
tag_name: 'v1.0.0',
|
||||
target_commitish: 'abc123',
|
||||
previous_tag_name: 'v0.9.0',
|
||||
});
|
||||
expect(createRelease).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag_name: 'v1.0.0',
|
||||
body: "Intro\n\n## What's Changed\n* Added support for previous_tag",
|
||||
generate_release_notes: false,
|
||||
}),
|
||||
);
|
||||
expect(createRelease.mock.calls[0][0]).not.toHaveProperty('previous_tag_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalizeRelease input_draft behavior', () => {
|
||||
const draftRelease: Release = {
|
||||
id: 1,
|
||||
@@ -340,6 +422,101 @@ describe('github', () => {
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('passes previous_tag_name through when creating a release with generated notes', async () => {
|
||||
const createReleaseSpy = vi.fn(async () => ({
|
||||
data: {
|
||||
id: 1,
|
||||
upload_url: 'test',
|
||||
html_url: 'test',
|
||||
tag_name: 'v1.0.0',
|
||||
name: 'test',
|
||||
body: 'generated notes',
|
||||
target_commitish: 'main',
|
||||
draft: true,
|
||||
prerelease: false,
|
||||
assets: [],
|
||||
},
|
||||
}));
|
||||
|
||||
await release(
|
||||
{
|
||||
...config,
|
||||
input_generate_release_notes: true,
|
||||
input_previous_tag: 'v0.9.0',
|
||||
},
|
||||
{
|
||||
getReleaseByTag: () => Promise.reject({ status: 404 }),
|
||||
createRelease: createReleaseSpy,
|
||||
updateRelease: () => Promise.reject('Not implemented'),
|
||||
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||
allReleases: async function* () {
|
||||
yield { data: [] };
|
||||
},
|
||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
deleteRelease: () => Promise.reject('Not implemented'),
|
||||
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
expect(createReleaseSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag_name: 'v1.0.0',
|
||||
generate_release_notes: true,
|
||||
previous_tag_name: 'v0.9.0',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes previous_tag_name through when updating a release with generated notes', async () => {
|
||||
const existingRelease: Release = {
|
||||
id: 1,
|
||||
upload_url: 'test',
|
||||
html_url: 'test',
|
||||
tag_name: 'v1.0.0',
|
||||
name: 'test',
|
||||
body: 'existing body',
|
||||
target_commitish: 'main',
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [],
|
||||
};
|
||||
const updateReleaseSpy = vi.fn(async () => ({ data: existingRelease }));
|
||||
|
||||
await release(
|
||||
{
|
||||
...config,
|
||||
input_generate_release_notes: true,
|
||||
input_previous_tag: 'v0.9.0',
|
||||
},
|
||||
{
|
||||
getReleaseByTag: () => Promise.resolve({ data: existingRelease }),
|
||||
createRelease: () => Promise.reject('Not implemented'),
|
||||
updateRelease: updateReleaseSpy,
|
||||
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||
allReleases: async function* () {
|
||||
yield { data: [existingRelease] };
|
||||
},
|
||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
deleteRelease: () => Promise.reject('Not implemented'),
|
||||
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
expect(updateReleaseSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
release_id: existingRelease.id,
|
||||
generate_release_notes: true,
|
||||
previous_tag_name: 'v0.9.0',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates published prereleases without the forced draft-first path', async () => {
|
||||
const prereleaseConfig = {
|
||||
...config,
|
||||
|
||||
@@ -174,6 +174,29 @@ describe('util', () => {
|
||||
});
|
||||
});
|
||||
describe('parseConfig', () => {
|
||||
const baseParsedConfig = {
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_previous_tag: undefined,
|
||||
input_make_latest: undefined,
|
||||
};
|
||||
|
||||
it('parses basic config', () => {
|
||||
assert.deepStrictEqual(
|
||||
parseConfig({
|
||||
@@ -186,27 +209,7 @@ describe('util', () => {
|
||||
INPUT_TARGET_COMMITISH: '',
|
||||
INPUT_DISCUSSION_CATEGORY_NAME: '',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
baseParsedConfig,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -216,25 +219,8 @@ describe('util', () => {
|
||||
INPUT_TARGET_COMMITISH: 'affa18ef97bc9db20076945705aba8c516139abd',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
...baseParsedConfig,
|
||||
input_target_commitish: 'affa18ef97bc9db20076945705aba8c516139abd',
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -244,25 +230,8 @@ describe('util', () => {
|
||||
INPUT_DISCUSSION_CATEGORY_NAME: 'releases',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_files: [],
|
||||
input_preserve_order: undefined,
|
||||
input_name: undefined,
|
||||
input_overwrite_files: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
...baseParsedConfig,
|
||||
input_discussion_category_name: 'releases',
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -273,25 +242,20 @@ describe('util', () => {
|
||||
INPUT_GENERATE_RELEASE_NOTES: 'true',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
...baseParsedConfig,
|
||||
input_generate_release_notes: true,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('supports an explicit previous tag for release notes generation', () => {
|
||||
assert.deepStrictEqual(
|
||||
parseConfig({
|
||||
INPUT_PREVIOUS_TAG: ' v1.2.3 ',
|
||||
}),
|
||||
{
|
||||
...baseParsedConfig,
|
||||
input_previous_tag: 'v1.2.3',
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -306,25 +270,11 @@ describe('util', () => {
|
||||
INPUT_TOKEN: 'input-token',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
...baseParsedConfig,
|
||||
github_token: 'input-token',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: false,
|
||||
input_prerelease: true,
|
||||
input_preserve_order: true,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -335,25 +285,8 @@ describe('util', () => {
|
||||
INPUT_TOKEN: ' ',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
...baseParsedConfig,
|
||||
github_token: 'env-token',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -365,25 +298,10 @@ describe('util', () => {
|
||||
INPUT_TOKEN: 'input-token',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
...baseParsedConfig,
|
||||
github_token: 'input-token',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: false,
|
||||
input_prerelease: true,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -394,25 +312,9 @@ describe('util', () => {
|
||||
INPUT_PRERELEASE: 'true',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
...baseParsedConfig,
|
||||
input_draft: false,
|
||||
input_prerelease: true,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -422,24 +324,7 @@ describe('util', () => {
|
||||
INPUT_MAKE_LATEST: 'false',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
input_append_body: false,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_name: undefined,
|
||||
input_overwrite_files: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
...baseParsedConfig,
|
||||
input_make_latest: 'false',
|
||||
},
|
||||
);
|
||||
@@ -450,25 +335,8 @@ describe('util', () => {
|
||||
INPUT_APPEND_BODY: 'true',
|
||||
}),
|
||||
{
|
||||
github_ref: '',
|
||||
github_repository: '',
|
||||
github_token: '',
|
||||
input_working_directory: undefined,
|
||||
...baseParsedConfig,
|
||||
input_append_body: true,
|
||||
input_body: undefined,
|
||||
input_body_path: undefined,
|
||||
input_draft: undefined,
|
||||
input_prerelease: undefined,
|
||||
input_preserve_order: undefined,
|
||||
input_files: [],
|
||||
input_overwrite_files: undefined,
|
||||
input_name: undefined,
|
||||
input_tag_name: undefined,
|
||||
input_fail_on_unmatched_files: false,
|
||||
input_target_commitish: undefined,
|
||||
input_discussion_category_name: undefined,
|
||||
input_generate_release_notes: false,
|
||||
input_make_latest: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ inputs:
|
||||
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
|
||||
required: false
|
||||
draft:
|
||||
description: "Creates a draft release. Defaults to false"
|
||||
description: "Keeps the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload."
|
||||
required: false
|
||||
prerelease:
|
||||
description: "Identify the release as a prerelease. Defaults to false"
|
||||
@@ -53,6 +53,10 @@ inputs:
|
||||
generate_release_notes:
|
||||
description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes."
|
||||
required: false
|
||||
previous_tag:
|
||||
description: "Optional. When generate_release_notes is enabled, use this tag as GitHub's previous_tag_name comparison base. If omitted, GitHub chooses the comparison base automatically."
|
||||
required: false
|
||||
default: ""
|
||||
append_body:
|
||||
description: "Append to existing body instead of overwriting it. Default is false."
|
||||
required: false
|
||||
|
||||
10
dist/index.js
vendored
10
dist/index.js
vendored
File diff suppressed because one or more lines are too long
176
src/github.ts
176
src/github.ts
@@ -31,37 +31,40 @@ export interface ReleaseResult {
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
type ReleaseNotesParams = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag_name: string;
|
||||
target_commitish: string | undefined;
|
||||
previous_tag_name?: string;
|
||||
};
|
||||
|
||||
type ReleaseMutationParams = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string | undefined;
|
||||
draft: boolean | undefined;
|
||||
prerelease: boolean | undefined;
|
||||
target_commitish: string | undefined;
|
||||
discussion_category_name: string | undefined;
|
||||
generate_release_notes: boolean | undefined;
|
||||
make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
previous_tag_name?: string;
|
||||
};
|
||||
|
||||
export interface Releaser {
|
||||
getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;
|
||||
|
||||
createRelease(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string | undefined;
|
||||
draft: boolean | undefined;
|
||||
prerelease: boolean | undefined;
|
||||
target_commitish: string | undefined;
|
||||
discussion_category_name: string | undefined;
|
||||
generate_release_notes: boolean | undefined;
|
||||
make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}): Promise<{ data: Release }>;
|
||||
createRelease(params: ReleaseMutationParams): Promise<{ data: Release }>;
|
||||
|
||||
updateRelease(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
release_id: number;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
body: string | undefined;
|
||||
draft: boolean | undefined;
|
||||
prerelease: boolean | undefined;
|
||||
discussion_category_name: string | undefined;
|
||||
generate_release_notes: boolean | undefined;
|
||||
make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}): Promise<{ data: Release }>;
|
||||
updateRelease(
|
||||
params: ReleaseMutationParams & {
|
||||
release_id: number;
|
||||
target_commitish: string;
|
||||
},
|
||||
): Promise<{ data: Release }>;
|
||||
|
||||
finalizeRelease(params: {
|
||||
owner: string;
|
||||
@@ -113,12 +116,7 @@ export class GitHubReleaser implements Releaser {
|
||||
return this.github.rest.repos.getReleaseByTag(params);
|
||||
}
|
||||
|
||||
async getReleaseNotes(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag_name: string;
|
||||
target_commitish: string | undefined;
|
||||
}): Promise<{
|
||||
async getReleaseNotes(params: ReleaseNotesParams): Promise<{
|
||||
data: {
|
||||
name: string;
|
||||
body: string;
|
||||
@@ -127,75 +125,55 @@ export class GitHubReleaser implements Releaser {
|
||||
return await this.github.rest.repos.generateReleaseNotes(params);
|
||||
}
|
||||
|
||||
private async prepareReleaseMutation<T extends ReleaseMutationParams>(
|
||||
params: T,
|
||||
): Promise<Omit<T, 'previous_tag_name'>> {
|
||||
const { previous_tag_name, ...releaseParams } = params;
|
||||
|
||||
if (
|
||||
typeof releaseParams.make_latest === 'string' &&
|
||||
!['true', 'false', 'legacy'].includes(releaseParams.make_latest)
|
||||
) {
|
||||
releaseParams.make_latest = undefined;
|
||||
}
|
||||
if (releaseParams.generate_release_notes) {
|
||||
const releaseNotes = await this.getReleaseNotes({
|
||||
owner: releaseParams.owner,
|
||||
repo: releaseParams.repo,
|
||||
tag_name: releaseParams.tag_name,
|
||||
target_commitish: releaseParams.target_commitish,
|
||||
previous_tag_name,
|
||||
});
|
||||
releaseParams.generate_release_notes = false;
|
||||
if (releaseParams.body) {
|
||||
releaseParams.body = `${releaseParams.body}\n\n${releaseNotes.data.body}`;
|
||||
} else {
|
||||
releaseParams.body = releaseNotes.data.body;
|
||||
}
|
||||
}
|
||||
releaseParams.body = releaseParams.body
|
||||
? this.truncateReleaseNotes(releaseParams.body)
|
||||
: undefined;
|
||||
return releaseParams;
|
||||
}
|
||||
|
||||
truncateReleaseNotes(input: string): string {
|
||||
// release notes can be a maximum of 125000 characters
|
||||
const githubNotesMaxCharLength = 125000;
|
||||
return input.substring(0, githubNotesMaxCharLength - 1);
|
||||
}
|
||||
|
||||
async createRelease(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
body: string | undefined;
|
||||
draft: boolean | undefined;
|
||||
prerelease: boolean | undefined;
|
||||
target_commitish: string | undefined;
|
||||
discussion_category_name: string | undefined;
|
||||
generate_release_notes: boolean | undefined;
|
||||
make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}): Promise<{ data: Release }> {
|
||||
if (
|
||||
typeof params.make_latest === 'string' &&
|
||||
!['true', 'false', 'legacy'].includes(params.make_latest)
|
||||
) {
|
||||
params.make_latest = undefined;
|
||||
}
|
||||
if (params.generate_release_notes) {
|
||||
const releaseNotes = await this.getReleaseNotes(params);
|
||||
params.generate_release_notes = false;
|
||||
if (params.body) {
|
||||
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
|
||||
} else {
|
||||
params.body = releaseNotes.data.body;
|
||||
}
|
||||
}
|
||||
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
|
||||
return this.github.rest.repos.createRelease(params);
|
||||
async createRelease(params: ReleaseMutationParams): Promise<{ data: Release }> {
|
||||
return this.github.rest.repos.createRelease(await this.prepareReleaseMutation(params));
|
||||
}
|
||||
|
||||
async updateRelease(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
release_id: number;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
body: string | undefined;
|
||||
draft: boolean | undefined;
|
||||
prerelease: boolean | undefined;
|
||||
discussion_category_name: string | undefined;
|
||||
generate_release_notes: boolean | undefined;
|
||||
make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}): Promise<{ data: Release }> {
|
||||
if (
|
||||
typeof params.make_latest === 'string' &&
|
||||
!['true', 'false', 'legacy'].includes(params.make_latest)
|
||||
) {
|
||||
params.make_latest = undefined;
|
||||
}
|
||||
if (params.generate_release_notes) {
|
||||
const releaseNotes = await this.getReleaseNotes(params);
|
||||
params.generate_release_notes = false;
|
||||
if (params.body) {
|
||||
params.body = `${params.body}\n\n${releaseNotes.data.body}`;
|
||||
} else {
|
||||
params.body = releaseNotes.data.body;
|
||||
}
|
||||
}
|
||||
params.body = params.body ? this.truncateReleaseNotes(params.body) : undefined;
|
||||
return this.github.rest.repos.updateRelease(params);
|
||||
async updateRelease(
|
||||
params: ReleaseMutationParams & {
|
||||
release_id: number;
|
||||
target_commitish: string;
|
||||
},
|
||||
): Promise<{ data: Release }> {
|
||||
return this.github.rest.repos.updateRelease(await this.prepareReleaseMutation(params));
|
||||
}
|
||||
|
||||
async finalizeRelease(params: {
|
||||
@@ -425,6 +403,11 @@ export const release = async (
|
||||
|
||||
const discussion_category_name = config.input_discussion_category_name;
|
||||
const generate_release_notes = config.input_generate_release_notes;
|
||||
const previous_tag_name = config.input_previous_tag;
|
||||
|
||||
if (generate_release_notes && previous_tag_name) {
|
||||
console.log(`📝 Generating release notes using previous tag ${previous_tag_name}`);
|
||||
}
|
||||
try {
|
||||
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
|
||||
|
||||
@@ -438,6 +421,7 @@ export const release = async (
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
maxRetries,
|
||||
previous_tag_name,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,6 +475,7 @@ export const release = async (
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
make_latest,
|
||||
previous_tag_name,
|
||||
});
|
||||
return {
|
||||
release: release.data,
|
||||
@@ -513,6 +498,7 @@ export const release = async (
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
maxRetries,
|
||||
previous_tag_name,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -796,6 +782,7 @@ async function createRelease(
|
||||
discussion_category_name: string | undefined,
|
||||
generate_release_notes: boolean | undefined,
|
||||
maxRetries: number,
|
||||
previous_tag_name: string | undefined,
|
||||
): Promise<ReleaseResult> {
|
||||
const tag_name = tag;
|
||||
const name = config.input_name || tag;
|
||||
@@ -822,6 +809,7 @@ async function createRelease(
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
make_latest,
|
||||
previous_tag_name,
|
||||
});
|
||||
const canonicalRelease = await canonicalizeCreatedRelease(
|
||||
releaser,
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface Config {
|
||||
input_target_commitish?: string;
|
||||
input_discussion_category_name?: string;
|
||||
input_generate_release_notes?: boolean;
|
||||
input_previous_tag?: string;
|
||||
input_append_body?: boolean;
|
||||
input_make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}
|
||||
@@ -114,6 +115,7 @@ export const parseConfig = (env: Env): Config => {
|
||||
input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
|
||||
input_discussion_category_name: env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
|
||||
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == 'true',
|
||||
input_previous_tag: env.INPUT_PREVIOUS_TAG?.trim() || undefined,
|
||||
input_append_body: env.INPUT_APPEND_BODY == 'true',
|
||||
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user