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
43 changes: 39 additions & 4 deletions .github/workflows/publish-crates.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Publish crates.io package

on:
release:
types:
- published
pull_request:
paths:
- ".github/workflows/publish-crates.yml"
Expand Down Expand Up @@ -41,7 +44,7 @@ env:
jobs:
dry-run:
name: Dry-run upbit-sdk publish
if: ${{ github.event_name != 'workflow_dispatch' || inputs.mode == 'dry-run' }}
if: ${{ github.event_name != 'release' && (github.event_name != 'workflow_dispatch' || inputs.mode == 'dry-run') }}
runs-on: ubuntu-latest

steps:
Expand All @@ -68,7 +71,7 @@ jobs:

publish:
name: Publish upbit-sdk
if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'publish' }}
if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.mode == 'publish') }}
runs-on: ubuntu-latest
environment: crates-io
env:
Expand All @@ -77,6 +80,8 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Select stable Rust toolchain
run: |
Expand All @@ -87,14 +92,44 @@ jobs:
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
echo "Real publish must be run from a release tag ref." >&2
echo "Real publish must run from a release tag ref." >&2
exit 1
fi
if [[ "${HAS_CARGO_REGISTRY_TOKEN}" != "true" ]]; then
echo "CARGO_REGISTRY_TOKEN secret is required for real publish." >&2
exit 1
fi
echo "Manual publish preconditions satisfied for ${GITHUB_REF_NAME}."

git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
tag_commit="$(git rev-parse "${GITHUB_REF_NAME}^{commit}")"
main_commit="$(git rev-parse "origin/main^{commit}")"

if ! git merge-base --is-ancestor "${tag_commit}" "${main_commit}"; then
echo "Release tag ${GITHUB_REF_NAME} must point to a commit already reachable from origin/main." >&2
exit 1
fi
Comment on lines +107 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: The ancestry check validates that the tag commit is an ancestor of the main commit, but semantically you want to ensure the tag points to a commit that has been merged into main. The current logic is inverted - it checks if tag_commit is an ancestor of main_commit, which would only be true if the tag was created before main advanced. This will fail for the intended workflow where you merge to main first, then tag that commit.

The correct check should verify that the tag commit is reachable from main, meaning main contains the tag commit in its history. This requires checking if main_commit is an ancestor of OR equal to tag_commit, or more directly, if they point to the same commit.

Suggested change
if ! git merge-base --is-ancestor "${tag_commit}" "${main_commit}"; then
echo "Release tag ${GITHUB_REF_NAME} must point to a commit already reachable from origin/main." >&2
exit 1
fi
if ! git merge-base --is-ancestor "${main_commit}" "${tag_commit}"; then
echo "Release tag ${GITHUB_REF_NAME} must point to a commit already reachable from origin/main." >&2
exit 1
fi


if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
event_action="$(jq -r '.action // ""' "${GITHUB_EVENT_PATH}")"
if [[ "${event_action}" != "published" ]]; then
echo "Release-triggered publishing only accepts the published action." >&2
exit 1
fi

release_target="$(jq -r '.release.target_commitish // ""' "${GITHUB_EVENT_PATH}")"
case "${release_target}" in
main|refs/heads/main|"${main_commit}"|"${tag_commit}")
;;
*)
echo "Release target_commitish must be main or the verified main/tag commit; got '${release_target}'." >&2
exit 1
;;
esac

echo "Release publish preconditions satisfied for ${GITHUB_REF_NAME} on main."
else
echo "Manual publish preconditions satisfied for ${GITHUB_REF_NAME} on main."
fi

- name: Check package metadata
run: cargo package -p upbit-sdk --allow-dirty --list
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ release checklist. The repository workflow `.github/workflows/publish-crates.yml
uses a reviewed pinned equivalent of `katyo/publish-crates@v2`; pull request,
integration-branch, main-branch, and manual dry-run paths cannot publish
because they run with `dry-run: true` and do not pass a registry token to the
third-party action. Real publishing requires a manual workflow dispatch with
`mode=publish`, a release tag ref, the `CARGO_REGISTRY_TOKEN` GitHub secret,
and the protected `crates-io` environment. See `docs/publishing.md` for the
full procedure.
third-party action. Real publishing is triggered when a GitHub Release is
published for a tag that is already reachable from `main`, and it requires the
`CARGO_REGISTRY_TOKEN` GitHub secret plus the protected `crates-io`
environment. See `docs/publishing.md` for the full procedure.

## Feature Scope

Expand Down
43 changes: 27 additions & 16 deletions docs/publishing.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# crates.io Publishing

This repository includes a GitHub Actions workflow for crates.io publish
readiness and controlled manual publishing of the `upbit-sdk` crate:
readiness and controlled publishing of the `upbit-sdk` crate after a GitHub
Release is published from `main`:

- workflow: `.github/workflows/publish-crates.yml`
- package path: `crates/upbit-sdk`
Expand All @@ -18,9 +19,10 @@ merge release branches unless the release is explicitly authorized.

The workflow runs in dry-run mode for pull requests, pushes to
`integration/BOG-223-upbit-rust-sdk`, pushes to `main`, and manual
`workflow_dispatch` runs where `mode` is `dry-run`. Pull request and push
dry-runs only trigger when the workflow or package paths listed in the workflow
change.
`workflow_dispatch` runs where `mode` is `dry-run`. GitHub Release events do
not run the dry-run job because they are reserved for the real publish path.
Pull request and push dry-runs only trigger when the workflow or package paths
listed in the workflow change.

Dry-run jobs:

Expand All @@ -36,29 +38,38 @@ The workflow prints an explicit dry-run confirmation before the action step.

## Real Publish Path

Real crates.io publishing is intentionally narrow. It can only happen when all
of these conditions are true:
Real crates.io publishing is intentionally narrow. The expected path is:
merge the release commit to `main`, create a release tag from `main`, then
publish a GitHub Release for that tag. The publish job can only proceed when
all of these conditions are true:

- the workflow is started manually with `workflow_dispatch`;
- the `mode` input is `publish`;
- the workflow is triggered by a GitHub Release `published` event, or a
separately authorized manual `workflow_dispatch` run with `mode=publish`;
- the selected workflow ref is a release tag;
- the release tag commit is already reachable from `origin/main`;
- for GitHub Release events, the event action is `published` and
`release.target_commitish` resolves to `main`, `refs/heads/main`, the
verified `origin/main` commit, or the verified tag commit;
- the repository has a `CARGO_REGISTRY_TOKEN` secret with crates.io publish
permissions for `upbit-sdk`;
- the `crates-io` GitHub Environment approvals and protections, if configured,
allow the job to continue.

The publish job validates the tag ref and token presence before invoking
the reviewed pinned `katyo/publish-crates@v2` equivalent with `dry-run: false`
and `check-repo: true`.
The publish job validates the tag ref, main-branch ancestry, release event
shape, and token presence before invoking the reviewed pinned
`katyo/publish-crates@v2` equivalent with `dry-run: false` and
`check-repo: true`.

Recommended release sequence:

1. Confirm the crate version, changelog, README, examples, and package include
list are approved.
2. Run the workflow manually with `mode=dry-run` on the release candidate ref.
3. Create the release tag only after release approval.
4. Run the workflow manually from that tag with `mode=publish`.
5. Verify the crates.io package page and published metadata after the job
3. Merge the approved release commit to `main`.
4. Create the release tag from `main`.
5. Publish the GitHub Release for that tag. The release `published` event starts
the real publish job.
6. Verify the crates.io package page and published metadata after the job
completes.

## Secret Handling
Expand All @@ -68,8 +79,8 @@ Store the crates.io token only as a GitHub Actions secret named
comments, workflow logs, screenshots, or local command output.

Dry-run jobs do not pass the registry token to the third-party action. The
token is only provided to the protected manual publish job after its tag and
secret preconditions pass.
token is only provided to the protected real publish job after its tag,
main-branch, release-event, and secret preconditions pass.

## Failure And Rollback Notes

Expand Down
Loading