Skip to content

Prepare a release candidate from a tag, without publishing one - #14

Open
owenpkent wants to merge 16 commits into
windows-updaterfrom
release-candidate-workflow
Open

owenpkent wants to merge 16 commits into
windows-updaterfrom
release-candidate-workflow

Conversation

@owenpkent

Copy link
Copy Markdown
Owner

Summary

The updater in #13 needs a feed to read, and nothing produced one. Pushing a v* tag now:

  1. Gates the tag against src/offloader/_version.py before spending a packaging run on it
  2. Builds the unsigned bundle and installer on windows-latest
  3. Confirms every artifact the release contract names actually exists
  4. Uploads them as a workflow artifact for inspection
  5. Prepares a draft prerelease pinned to the tagged commit, with notes and no assets

It attaches nothing, deliberately

This is the design, not a gap. Signing needs the hardware token, exposed only to an interactive session on the release workstation, and docs/release-plan.md is explicit: signing is mandatory for any Windows release including a beta, and unsigned CI output must never be promoted to a download. So the workflow does everything that does not need the key and stops. The signed installer is uploaded separately, and the draft notes carry that command.

test_nothing_it_builds_is_attached_to_a_release asserts that property. It is one line away from being wrong at any time, and wrong in a way no test of the build itself would catch.

Why the gate is a script

scripts/check_tag.py rather than inline YAML, so it is testable and can run before pushing a tag. It shares one version grammar with the updater and with the installer's Windows version fields, so a tag that cannot be published (latest, v1.2, v1.2.3-evil) is refused here instead of producing an asset nothing can compare.

A tag disagreeing with the version literal is the mistake worth catching early, because it does not look like a failure later: the release publishes, the installer installs, and the fault surfaces as an update every installed copy declines — the updater compares the feed's tag against the version compiled into the installer, so the mismatch only appears as a silently broken update path.

Two bugs I caught before they shipped

  • Both my first-draft heredocs (<<'PY' and <<EOF) had indented terminators, which never close once YAML dedents the block scalar. Replaced with a one-line python -c and a script. I syntax-checked every run: block with bash -n rather than trusting it.
  • The notes used cmd-style ^ continuations while this project documents PowerShell. Now single-line.

scripts/release_notes.py exists because prose with backticks and blank lines inside a YAML block scalar inside a shell heredoc has three levels of quoting to get wrong, and the failure mode is a note that truncates at the first surprise. It also refuses a tag that contradicts the version, since the heading names the tag while the instructions name filenames built from the version.

Other decisions

  • contents: write is held only by the drafting job. The job running third-party packaging tools holds no token that can publish.
  • Re-running a tag refreshes the notes rather than recreating the release, so a signed asset already uploaded against it is not discarded.
  • workflow_dispatch rehearses the checks without touching releases, guarded by a refs/tags/v condition on the drafting job.
  • Setup mirrors the existing windows-bundle CI job exactly (Python 3.12, NSIS 3.12.0 via choco), since that combination is already proven on the runner.

Not parity

Alpha-OSK has no release automation: releases are built locally with the token and published by hand with gh release create. There was nothing to port, so this is new work. Noted in the release plan so the next reader does not go looking for a reference implementation.

Test plan

  • tests/test_release_workflow.py + tests/test_check_tag.py — 28 tests: tag trigger, no-asset-upload, draft-is-draft, permissions scoped to the drafting job, --no-sign, gate ordered before the build, dispatch cannot touch releases, notes content, notes refusing a contradictory tag
  • Workflow YAML parses; all three run: blocks pass bash -n
  • Gate exercised live: check_tag.py v0.1.0 exits 0, v9.9.9 exits 1 naming both versions, latest exits 2
  • Notes rendered and read end to end
  • Full suite 810 passed, 12 skipped; ruff check src tests scripts build/windows clean
  • The workflow itself runs only on a real tag push; not exercised here

Pushing a v* tag now gates the tag against the declared version, builds the
unsigned bundle and installer, confirms every artifact the release contract
names exists, and prepares a draft prerelease pinned to the tagged commit.

It attaches nothing, and that is the design rather than a gap. Signing needs
the hardware token, which is only exposed to an interactive session on the
release workstation, and the release plan requires every Windows download to
be signed including a beta. So the workflow does everything that does not
need the key and stops. A test asserts the negative property the whole
arrangement rests on: no job attaches what it built to a release. That is one
line away from being wrong at any time, and wrong in a way no test of the
build itself would notice.

The tag gate is a script rather than inline YAML so it is testable and can be
run before pushing. It shares one version grammar with the updater and with
the installer's Windows version fields, so a tag that cannot be published is
refused here instead of producing an asset nothing can compare. A tag that
disagrees with the version literal is the mistake worth catching early: the
release would publish, the installer would install, and the fault would
surface later as an update every installed copy declines, because the updater
compares the feed's tag against the version compiled into the installer.

The draft notes are generated by a script too. Prose with backticks and blank
lines inside a YAML block scalar inside a shell heredoc has three levels of
quoting to get wrong, and the failure mode is a note that truncates at the
first surprise. It refuses a tag that contradicts the version, since the
heading names the tag while the instructions name filenames built from the
version.

Write permission is held only by the drafting job; the job that runs
third-party packaging tools has no token that can publish. Re-running a tag
refreshes the notes rather than recreating the release, so a signed asset
already uploaded against it is not discarded. A workflow_dispatch run
rehearses the checks without touching releases.

Alpha-OSK has no release automation to copy: its releases are built locally
and published by hand. This is new work rather than parity.

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed the tag gate, candidate checkout/build, artifact contract, generated draft instructions, and release mutation conditions. Three workflow cases need correction: rehearsing a proposed tag, dispatching from an existing tag, and classifying stable releases. Details and regression suggestions are inline.

This was a static workflow review. No workflow was dispatched, tag pushed, release changed, installer signed, or artifact published during validation.

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Three ways the workflow did not do what it says it does.

The dispatch input was used as the checkout ref as well as the version to
gate. Entering a proposed vX.Y.Z therefore failed in checkout before
check_tag.py could say anything about it, which is the whole documented
purpose of the rehearsal. The run now checks out whatever commit it was
started from - the tagged commit on a push, the selected branch on a
dispatch - and the input reaches only the gate. A tag is a version to
validate, not a ref to fetch.

The drafting job was guarded on the ref alone. A dispatch can be started
against an existing tag, and github.ref is a tag ref then too, so a
rehearsal reached the job with contents: write, created or edited the
release, and passed --draft to one that had already been published. It now
requires the event to be a push as well.

--prerelease was passed unconditionally, while check_tag.py accepts stable
tags. Publishing v1.0.0 as a prerelease leaves the installer outside
GitHub's /releases/latest, which is the feed the updater reads, so every
installed copy would go on declining the release meant for them. The
classification comes from the version the gate validated, and is set
explicitly on both the create and the refresh path, so a rerun corrects an
existing draft rather than inheriting whatever the first run chose.

check_tag.py writes version and prerelease to GITHUB_OUTPUT itself, so the
facts the draft is built from come from the step that validated them rather
than a second reading that could disagree. It writes nothing when the gate
fails.

Tests: the drafting condition evaluated against all three events rather than
matched as text, since the defect was a condition that read correctly and
was true in a case nobody had enumerated; the checkout ref not being the
input and the gate still receiving it; the classification of six versions;
and the outputs written on a pass and absent on a refusal.

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Follow-up review of c8d94af against the earlier workflow findings.

The proposed-tag rehearsal now checks out the selected ref, dispatch cannot enter the release-writing job, and stable/prerelease classification comes from the version gate. One remaining published-release case needs protection: rerunning the original tag-push workflow still takes the edit path with --draft. Details inline.

Validation: 22 tag-gate tests passed. The workflow test module was skipped because PyYAML is not installed in this environment; no dependency was added. The workflow and release-edit command were inspected statically. No workflow, tag, or release was changed during validation.

Comment thread .github/workflows/release.yml Outdated

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No additional actionable defect found in this incremental diff.

Reviewed the event/ref guards, tag/version agreement, workflow permissions, build/artifact paths, draft lifecycle and generated publication instructions. Previously reported dispatch handling, prerelease classification and protection of already-published releases have corresponding guards and regression coverage in this head.

Validation: 41 tag/workflow/release-note tests passed in a separate pinned source export; three POSIX bash execution tests skipped on Windows. Those shell branches were also inspected statically, with successful Linux CI providing separate evidence. Current-head CI succeeds and the diff check passes.

Limits: no tag created, workflow dispatched, release edited/published or signed candidate produced. The review verifies workflow logic and tests, not successful end-to-end release qualification.

Pinned scope: declared base bf69fd36cd7923b6b09476963cb6d7250aa0fe7a, merge base bf69fd36cd7923b6b09476963cb6d7250aa0fe7a, head 19c7be0735b097d76a4f047c6a9bf469b1d10f76. Existing discussion and current-head checks were reviewed; no threads were resolved and no formal approval or merge is being issued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant