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
4 changes: 4 additions & 0 deletions changelog.d/+release-on-existing-tag.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
`publish-github-release` creates the draft on the existing tag without naming a target commit. GitHub compared the
target's workflow files with the default branch tip and refused the job's token with
`Resource not accessible by integration` whenever a pull request that changed a workflow merged while the release was
building. The Action now fails before creating the draft if the tag is missing or points at another commit.
2 changes: 1 addition & 1 deletion docs/release-publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ needs no checkout or tool installation. Enable immutable releases in the reposit
| Input | Default | Purpose |
| --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tag` **(required)** | | Tag of the Release, in the caller repository. |
| `commit` **(required)** | | The tag's commit, used as target when the Action creates the Release. |
| `commit` **(required)** | | The tag's commit. The Action stops before creating the Release if the tag is missing or names another commit. |
| `title` **(required)** | | Release title. |
| `notes-file` **(required)** | | The exact rendered notes file. |
| `assets-directory` | empty | Assets to attach. Each filename and SHA256 is compared with GitHub's asset metadata; missing draft assets are uploaded, extra or changed ones rejected. After publication it verifies GitHub's release attestation for each. |
Expand Down
5 changes: 5 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ A publish call failed, and reading the Release back did not show the expected st
resumes the draft. A published Release with a different asset set needs manual inspection, because published releases
are not rewritten.

**`tag <tag> must exist and point at <commit>, found ...`**

The Action creates the Release on an existing tag, and the tag is missing or names another commit than `commit`. Push
the tag before publishing and pass the commit it points at, such as `github.sha` in a workflow triggered by the tag.

**`release asset attestation could not be verified`**

GitHub has no release attestation for a local asset. Check that immutable releases are enabled for the repository.
Expand Down
14 changes: 11 additions & 3 deletions publish-github-release/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,18 @@ runs:
# shellcheck disable=SC1091 # GitHub supplies this Action path at runtime.
source "$GITHUB_ACTION_PATH/retry.sh"
if jq -e 'length == 0' "$RUNNER_TEMP/release-existing.json" >/dev/null; then
jq -n --arg tag "$TAG" --arg commit "$COMMIT" --arg title "$TITLE" \
# The request names no target: GitHub checks a target's workflow files against the default branch tip and
# refuses a GITHUB_TOKEN, which never holds the workflows permission, once a later merge changed one. The tag
# must then exist, or publishing would create it at the default branch tip.
tagged=$(gh_api "repos/$GH_REPO/commits/refs/tags/$TAG" --jq .sha) || tagged=""
if [[ "$tagged" != "$COMMIT" ]]; then
echo "::error::tag $TAG must exist and point at $COMMIT, found ${tagged:-no commit}" >&2
exit 1
fi
jq -n --arg tag "$TAG" --arg title "$TITLE" \
--rawfile body "$RUNNER_TEMP/release-body.md" --argjson prerelease "$PRERELEASE" \
'{tag_name: $tag, target_commitish: $commit, name: $title, body: $body,
draft: true, prerelease: $prerelease}' >"$RUNNER_TEMP/release-request.json"
'{tag_name: $tag, name: $title, body: $body, draft: true, prerelease: $prerelease}' \
>"$RUNNER_TEMP/release-request.json"
# Not retried: if GitHub stored the draft before answering 5xx, a second POST could create another.
gh api -X POST "repos/$GH_REPO/releases" --input "$RUNNER_TEMP/release-request.json" \
>"$RUNNER_TEMP/release-created.json"
Expand Down
46 changes: 45 additions & 1 deletion tests/publish_github_release/test_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ def _expect_generated_notes(server: HTTPServer) -> None:
)


def _expect_tag(server: HTTPServer, commit: str) -> None:
server.expect_ordered_request(
"/api/v3/repos/tox-dev/fixture/commits/refs/tags/0.1.0", method="GET"
).respond_with_json({"sha": commit})


def _release_requests(server: HTTPServer) -> list[Request]:
return [request for request, _ in server.log if request.path != _GENERATE_NOTES]

Expand Down Expand Up @@ -317,6 +323,7 @@ def test_missing_release_is_created_then_optionally_published(
prefix = "/api/v3/repos/tox-dev/fixture"
server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json([])
_expect_generated_notes(server)
_expect_tag(server, commit)
server.expect_ordered_request(f"{prefix}/releases", method="POST").respond_with_json(draft, status=201)
server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(draft)
if mode != "stage":
Expand All @@ -326,6 +333,7 @@ def test_missing_release_is_created_then_optionally_published(
result = _request(repository, tmp_path, action_scripts, notes, commit, environment | release_cli, mode=mode)
assert result.returncode == 0, result.stderr
assert [request.method for request in _release_requests(server)] == [
"GET",
"GET",
"POST",
"GET",
Expand All @@ -335,7 +343,6 @@ def test_missing_release_is_created_then_optionally_published(
request = next(request for request in _release_requests(server) if request.method == "POST")
assert request.get_json() == {
"tag_name": "0.1.0",
"target_commitish": commit,
"name": "0.1.0",
"body": notes.read_text(),
"draft": True,
Expand Down Expand Up @@ -758,6 +765,7 @@ def test_missing_release_explains_created_draft(
prefix = "/api/v3/repos/tox-dev/fixture"
server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json([])
_expect_generated_notes(server)
_expect_tag(server, commit)
server.expect_ordered_request(f"{prefix}/releases", method="POST").respond_with_json(draft, status=201)
server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(draft)

Expand Down Expand Up @@ -806,6 +814,7 @@ def test_new_release_credits_first_time_contributors_above_the_links(
"**Full Changelog**: https://github.com/tox-dev/fixture/compare/0.0.9...0.1.0\n"
}
)
_expect_tag(server, commit)
server.expect_ordered_request(f"{prefix}/releases", method="POST").respond_with_json(draft, status=201)
server.expect_ordered_request(f"{prefix}/releases/42", method="GET").respond_with_json(draft)

Expand Down Expand Up @@ -842,6 +851,7 @@ def test_server_error_on_generated_notes_is_retried(
body, status=status, mimetype=mimetype
)
_expect_generated_notes(server)
_expect_tag(server, commit)
server.expect_ordered_request(f"{prefix}/releases", method="POST").respond_with_json(
_release(notes, draft=True), status=201
)
Expand Down Expand Up @@ -871,6 +881,39 @@ def test_client_error_on_generated_notes_is_not_retried(
assert len(_requests(server, "POST", _GENERATE_NOTES)) == 1


@pytest.mark.parametrize(
("status", "body", "found"),
[
pytest.param(200, {"sha": "0" * 40}, "0" * 40, id="other-commit"),
pytest.param(422, {"message": "No commit found for SHA: refs/tags/0.1.0"}, "no commit", id="missing"),
],
)
def test_draft_creation_requires_the_tag_on_the_commit(
repository: Path,
tmp_path: Path,
action_scripts: Path,
github_api: tuple[HTTPServer, dict[str, str]],
release_cli: dict[str, str],
status: int,
body: dict[str, str],
found: str,
) -> None:
server, environment = github_api
notes, commit = _notes_and_commit(repository, tmp_path)
prefix = "/api/v3/repos/tox-dev/fixture"
server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json([])
_expect_generated_notes(server)
server.expect_ordered_request(f"{prefix}/commits/refs/tags/0.1.0", method="GET").respond_with_json(
body, status=status
)

result = _request(repository, tmp_path, action_scripts, notes, commit, environment | release_cli, mode="stage")

assert result.returncode != 0
assert f"::error::tag 0.1.0 must exist and point at {commit}, found {found}" in result.stderr
assert not _requests(server, "POST", f"{prefix}/releases")


def test_draft_creation_is_not_retried(
repository: Path,
tmp_path: Path,
Expand All @@ -883,6 +926,7 @@ def test_draft_creation_is_not_retried(
prefix = "/api/v3/repos/tox-dev/fixture"
server.expect_ordered_request(f"{prefix}/releases", query_string="per_page=100", method="GET").respond_with_json([])
_expect_generated_notes(server)
_expect_tag(server, commit)
server.expect_ordered_request(f"{prefix}/releases", method="POST").respond_with_data(status=502)
result = _request(repository, tmp_path, action_scripts, notes, commit, environment | release_cli, mode="stage")
assert result.returncode != 0
Expand Down