diff --git a/.github/workflows/published-release-assets.yml b/.github/workflows/published-release-assets.yml new file mode 100644 index 0000000..2133009 --- /dev/null +++ b/.github/workflows/published-release-assets.yml @@ -0,0 +1,128 @@ +name: Prepare draft release assets + +on: + workflow_dispatch: + inputs: + tag: + description: Existing version tag for the draft release, for example v0.1.0 + required: true + type: string + draft_release_id: + description: Numeric GitHub release ID of the unpublished draft for that tag + required: true + type: string + +permissions: + contents: read + actions: read + +concurrency: + group: draft-release-assets-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + compatibility: + name: ${{ matrix.base_cli.name }} / Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + RELEASE_TAG: ${{ inputs.tag }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.13"] + base_cli: + - name: minimum-0.4.3 + spec: "==0.4.3" + - name: latest-supported + spec: ">=0.4.3,<0.5" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + ref: ${{ inputs.tag }} + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + - name: Verify the release tag matches VERSION + run: test "$(tr -d '[:space:]' < VERSION)" = "${RELEASE_TAG#v}" + - name: Build the released demo wheel + run: python -m pip wheel --no-deps --wheel-dir dist . + - name: Install the framework version under test + run: python -m pip install "base-cli${{ matrix.base_cli.spec }}" "pytest>=8,<9" + - name: Install the release wheel without source dependencies + run: python -m pip install --no-deps dist/*.whl + - name: Run installed-wheel consumer tests + run: python -m pytest -q + - name: Record the exact compatibility evidence + env: + PYTHON_VERSION: ${{ matrix.python-version }} + BASE_CLI_REQUEST: ${{ matrix.base_cli.spec }} + run: | + python - <<'PY' > compatibility.txt + from importlib.metadata import version + import os + + print(f"python={os.environ['PYTHON_VERSION']}") + print(f"base-cli-request={os.environ['BASE_CLI_REQUEST']}") + print(f"base-cli-installed={version('base-cli')}") + print(f"base-cli-demo-installed={version('base-cli-demo')}") + PY + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: compatibility-${{ matrix.base_cli.name }}-python-${{ matrix.python-version }} + path: compatibility.txt + if-no-files-found: error + + assets: + needs: [compatibility] + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + actions: read + contents: write + env: + RELEASE_TAG: ${{ inputs.tag }} + DRAFT_RELEASE_ID: ${{ inputs.draft_release_id }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + ref: ${{ inputs.tag }} + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - name: Verify the release tag matches VERSION + run: test "$(tr -d '[:space:]' < VERSION)" = "${RELEASE_TAG#v}" + - name: Verify the selected release is an unpublished draft for this tag + run: | + test "$(gh api "repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" --jq '.draft')" = true + test "$(gh api "repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" --jq '.prerelease')" = false + test "$(gh api "repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" --jq '.tag_name')" = "$RELEASE_TAG" + - name: Install package and release checks + run: python -m pip install ".[dev]" + - name: Run the authoritative consumer and package gates + run: | + ./tests/validate.sh + ./tests/package.sh + - name: Build the release distributions + run: python -m build --sdist --wheel --outdir release-dist . + - name: Verify distribution metadata + run: python -m twine check release-dist/* + - name: Install and test the built release wheel + run: | + python -m pip install --no-deps release-dist/*.whl + python -m pytest -q + - name: Download compatibility evidence from this workflow run + run: gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --pattern 'compatibility-*' --dir release-dist/compatibility-evidence + - name: Prepare evidence and immutable checksums + run: | + { + printf 'Release tag: %s\n' "$RELEASE_TAG" + printf 'Declared Base-CLI range: base-cli>=0.4.3,<0.5\n\n' + find release-dist/compatibility-evidence -name compatibility.txt -print -exec cat {} \; + } > release-dist/COMPATIBILITY.txt + sha256sum release-dist/*.whl release-dist/*.tar.gz release-dist/COMPATIBILITY.txt > release-dist/SHA256SUMS.txt + - name: Attach verified assets to the unpublished draft + run: gh release upload "$RELEASE_TAG" release-dist/*.whl release-dist/*.tar.gz release-dist/COMPATIBILITY.txt release-dist/SHA256SUMS.txt --clobber --repo "$GITHUB_REPOSITORY" diff --git a/README.md b/README.md index f6993ca..993fd85 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,19 @@ $ northstar --help $ northstar --quiet status ``` +### Install an existing release + +To install the first published demo release without cloning the repository: + +```bash +python -m pip install \ + "https://github.com/basefoundry/base-cli-demo/releases/download/v0.1.0/base_cli_demo-0.1.0-py3-none-any.whl" +``` + +Verify the artifact against `SHA256SUMS.txt` on the same GitHub Release. The +wheel depends on the released `base-cli>=0.4.3,<0.5` API line; it does not +install Base or require a Base workspace. + The default environment is `dev`. Select another fixture environment with the framework lifecycle option: diff --git a/docs/release-process.md b/docs/release-process.md index 8ba3069..c0d17a9 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,9 +1,9 @@ # Release Process This repository uses the Base release contract. The machine-readable release -metadata lives in `base_manifest.yaml`; the guarded `basectl release` -commands use that contract for readiness checks, notes, tags, and GitHub -Releases. +metadata lives in `base_manifest.yaml`; `basectl release check/plan/notes` use +that contract for readiness and release notes. GitHub immutable releases must +be prepared as drafts so validated assets are attached before publication. ## Standard Sequence @@ -23,29 +23,44 @@ Releases. ```bash basectl release check --version X.Y.Z basectl release plan --version X.Y.Z - basectl release notes --version X.Y.Z + basectl release notes --version X.Y.Z > RELEASE_NOTES.md basectl release publish --version X.Y.Z --dry-run ``` -7. Publish only after the checks pass. Use `--yes` only from a trusted - non-interactive release shell: +7. After separate publication authorization, verify immutable releases are + enabled for the repository, then create the annotated version tag for the + exact reviewed `main` commit. Create a GitHub Release draft for that existing + tag and add the release notes from step 6 (`RELEASE_NOTES.md`). Do not run + `basectl release publish --yes` for this immutable-release path: it creates + a published release directly, leaving no draft stage for attaching the + validated assets. ```bash - basectl release publish --version X.Y.Z --yes + git tag -a vX.Y.Z -m "base-cli-demo vX.Y.Z" + git push origin vX.Y.Z + gh release create vX.Y.Z --verify-tag --draft --title "vX.Y.Z" --notes-file RELEASE_NOTES.md ``` -8. Verify the annotated tag and GitHub Release for `basefoundry/base-cli-demo`. -9. The `Release package` workflow validates the tagged version, runs the - package gate, installs the built wheel, and uploads the wheel/source - distributions as a workflow artifact. Base's `basectl release publish` - remains the guarded publisher for GitHub Release notes; this demo workflow - does not imply a PyPI or Base-CLI release. -10. Complete every declared downstream handoff. For Homebrew, update the tap +8. Dispatch `Prepare draft release assets` with the tag and draft release ID + (`gh release view vX.Y.Z --json databaseId --jq .databaseId`). The workflow + verifies that the selected release is still a draft for that tag, tests the + wheel against the minimum and latest supported Base-CLI releases on Python + 3.10 and 3.13, then attaches the wheel, sdist, exact compatibility evidence, + and SHA-256 checksums. It never creates a tag or release and never publishes + the draft. +9. Review the draft assets and verify the repository has immutable releases + enabled. Then publish the draft in GitHub. When immutable releases are + enabled, GitHub locks the tag and assets at publication; see the official + [immutable releases guidance](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). +10. Confirm the README's version-pinned installation path in a clean + environment, download the checksum/evidence files, and verify package + filenames and hashes against the published assets. +11. Complete every declared downstream handoff. For Homebrew, update the tap formula to the published archive and checksum, run the formula tests and audit, publish required bottles, and verify install and upgrade paths. If a downstream repository pins this project by commit, update and validate that pin after the release. -11. Record the release and downstream URLs on the release issue, then remove +12. Record the release and downstream URLs on the release issue, then remove the release worktree and merged branches when safe. ## Repository Contract