feat(ci): release & distribution pipeline — ADR 0003 + scaffold workflow#405
feat(ci): release & distribution pipeline — ADR 0003 + scaffold workflow#405Matovidlo wants to merge 5 commits into
Conversation
|
@claude review |
3c9c953 to
fc19fa4
Compare
There was a problem hiding this comment.
Pull request overview
Adds a proposed ADR and a new tag-triggered GitHub Actions workflow scaffold to define and begin implementing kbagent’s future release/distribution CI/CD (wheel build → PyPI via OIDC, later Nuitka freeze + packaging), while keeping publishing steps stubbed for safety.
Changes:
- Add ADR 0003 describing the target release/distribution pipeline, rollout phases, and required release secrets.
- Add a scaffold
release-kbagentworkflow with gated checks/tests, wheel build + artifact upload, and stubbed publish/freeze/package stages. - Introduce a release pipeline structure intended to align with existing CI jobs (but currently not fully mirrored).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
docs/adr/0003-release-distribution-cicd.md |
New ADR documenting the planned release & distribution CI/CD approach and rollout. |
.github/workflows/release-kbagent.yml |
New tag-based release workflow scaffold (gating, wheel build, stubbed publish/freeze/package jobs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
429d7f0 to
f0c5cd4
Compare
|
Oh, it's funny I was going to address a similar thing, can you please hold this PR until I send another commit or a comment for your agent to this PR pls? 🙏 |
|
@soustruh yes feel free |
7e73ab3 to
a3d25f8
Compare
Decision record for shipping kbagent as a self-contained native binary (PyInstaller) packaged for brew/apt/dnf/apk/chocolatey/winget under the keboola-cli2 name, coexisting with the legacy Go kbc. Covers PyPI OIDC publishing, the nfpm packaging choice (not goreleaser), signing/notarization, and the required release secrets.
…+ nfpm) Tag-triggered (v*.*.*) pipeline that freezes kbagent into a self-contained native binary and publishes it across all major package managers, giving the same install UX as the legacy Go kbc without requiring Python/uv on the user's machine. workflow_dispatch is a dry run (freeze + sign + package only); external publishing is gated on refs/tags/* and the protected release env. - .github/workflows/release-kbagent.yml: version → gate → pypi/freeze(matrix) → package-linux → github-release/publish-s3 → homebrew/chocolatey/winget → test-install. Pre-release (PEP 440 b/rc/.dev) tags build + GitHub pre-release only; gate fails fast if the tag version != pyproject.toml. - .github/actions/setup-build: composite pinning uv + Python 3.12 (+ optional Node). - build/package: PyInstaller entry point, nfpm config (deb/rpm/apk), per-OS sign/notarize (macOS notarytool, Windows Azure Key Vault + jsign), signed apt/rpm/apk repo indexing, Homebrew formula + Chocolatey wrappers. - .gitignore: track build/package/ (packaging sources) while still ignoring build artifacts.
a3d25f8 to
2b7a6af
Compare
padak
left a comment
There was a problem hiding this comment.
Review of #405 — feat(ci): release & distribution pipeline — ADR 0003 + scaffold workflow
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR ships a complete tag-triggered release pipeline (release-kbagent.yml) that freezes a self-contained kbagent binary via PyInstaller for linux amd64/arm64, macOS arm64, and Windows amd64, then packages and publishes via nfpm (deb/rpm/apk), Homebrew, Chocolatey, WinGet, and PyPI. It also adds an ADR (0003), the composite setup-build action, packaging config under build/package/, and a .gitignore update. The PR description correctly classifies this as CI + docs + packaging only — no application source changes, no new CLI commands — so the Plugin synchronization map is not applicable. make check passes cleanly (4124 passed, 8 skipped). The overall design is solid: real jobs, correct gating, supply-chain-conscious (nfpm hash-verified, jsign hash-verified, wingetcreate Authenticode-checked before execution). Three non-blocking issues and four nits need attention. Verdict: COMMENT (no blocking findings).
Verdict
- Verdict: COMMENT
- Blocking findings: 0
- Non-blocking findings: 3
- Nits: 4
Blocking findings
(none)
Non-blocking findings
[NB-1] .github/workflows/release-kbagent.yml:74-76 — workflow-level id-token: write + contents: write granted to every job
Top-level permissions: propagates id-token: write (OIDC token capable of assuming AWS/PyPI roles) and contents: write (can create/delete GitHub Releases and push tags) to jobs that need neither — specifically version, gate, freeze (signing only), and package-linux. The existing release.yml in this repo scopes contents: write at the workflow level too, so this matches the current pattern, but the release workflow is a higher-value target: it runs on tag pushes and mints OIDC tokens worth real-world credentials.
Fix: scope permissions per-job — give contents: write only to github-release, id-token: write only to pypi and publish-s3. Every other job can run with the default contents: read / no OIDC token. This is the GitHub recommended pattern for release workflows.
[NB-2] docs/adr/0003-release-distribution-cicd.md:908 — ADR claims "macOS amd64/arm64" but the workflow only builds arm64
The ADR's decision prose ("Freezes native kbagent binaries... across a matrix (linux amd64/arm64, macOS amd64/arm64, windows amd64)") says two macOS arches, but the freeze matrix in .github/workflows/release-kbagent.yml:163-168 has a single { os: macos-latest, platform: darwin, arch: arm64 } entry with an explicit comment that Intel Macs are excluded. The Homebrew formula correctly handles this with an on_intel do / odie block. The inconsistency is in the ADR text only — the workflow is intentionally correct — but the ADR will mislead future contributors who read it.
Fix: change the ADR line 908 to "macOS arm64 (Apple Silicon only; Intel Macs fall back to uv tool install)" and reference the macos-13 comment in the workflow for re-enabling the Intel leg.
[NB-3] docs/adr/0003-release-distribution-cicd.md:958 — ADR secrets table lists DEB_KEY_PUBLIC but it is not used
The secrets table in the ADR (line 958) lists DEB_KEY_PRIVATE / DEB_KEY_PUBLIC as a required secret pair. However build/package/linux/index.sh:621 explicitly documents that the apt keyring (keboola.gpg) is derived from DEB_KEY_PRIVATE (via gpg --export) and there is no DEB_KEY_PUBLIC variable referenced anywhere in the scripts or workflow jobs. The publish-s3 job (release-kbagent.yml:313) only passes DEB_KEY_PRIVATE. Whoever sets up the release environment secrets will add DEB_KEY_PUBLIC unnecessarily, or worse, wonder why the workflow never consumes it and assume the table is stale.
Fix: Remove DEB_KEY_PUBLIC from the ADR secrets table and add a parenthetical "(public half exported inline from DEB_KEY_PRIVATE by index.sh)".
Nits
-
[NIT-1]docs/adr/0003-release-distribution-cicd.md:5— ADR status isProposedbut it will beAccepted(merged). Convention in this repo: ADR 0001 and 0002 both sayAccepted. Update toAcceptedbefore merge. -
[NIT-2].github/workflows/release-kbagent.yml:336—cpina/github-action-push-to-another-repository@v1.7.2is pinned to a version tag, not a commit SHA. The official GitHub hardening guide recommends SHA pins for third-party actions to avoid tag mutable-repoint attacks. Thesoftprops/action-gh-release@v2,pypa/gh-action-pypi-publish@release/v1, andaws-actions/configure-aws-credentials@v6have the same pattern. Theactions/*actions (GitHub-owned) are conventionally considered safe at@v5/@v6, but the third-party ones (cpina,softprops,pypa,aws-actions) are higher-risk. At minimum, pincpina/github-action-push-to-another-repositoryto a SHA given that it receivesAPI_TOKEN_GITHUBwith write access to the tap repo. -
[NIT-3].github/workflows/release-kbagent.yml:202-203—APPLE_ACCOUNT_USERNAME: apple@keboola.comandAPPLE_TEAM_ID: 46P6KJ65M2are hardcoded as literals in the workflow'senv:block rather than referencing secrets or workflow-level env vars. Apple Team IDs and Apple ID emails are not secret, but embedding them as literals in the workflow makes it harder to rotate (requires a PR vs a repo-secret change). Moving them to the workflow-levelenv:block or to repo variables (vars.APPLE_TEAM_ID) would keep them in one place and make rotation easier without needing a code change. -
[NIT-4]build/package/linux/index.sh:643—abuild-signandapk-toolsinstallation uses a singleapt-get install ... || apt-get install ...fallback pattern that silently skips the apk tooling if it's unavailable, then the downstream APK index block re-checks withcommand -v abuild-sign. This is correct but the warning message at line 686 says "skipping apk index (deb/rpm done)" without an explicit exit-0 signal. Ifabuild-signis absent the workflow continues silently — which is the intended behaviour for pre-release, but a real release withIS_PRERELEASE == falseskips the APK index without any error. Consider checkingAPK_KEY_PRIVATEis set ANDabuild-signis available at the top of the function, andexit 1on a real release if the tooling is missing.
Verification log
gh auth status→ authenticated aspadakongithub.com✓gh pr view 405 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state→ 15 files, +898/-1, state=OPEN ✓git rev-parse --abbrev-ref HEADaftergh pr checkout 405 --detach→ detached at2b7a6af✓CONTRIBUTING.mdPlugin synchronization map read ✓ — no new CLI commands in diff; plugin sync map not applicable; confirmed viagrep -E '@.*_app\.command' /tmp/kbagent-pr-405.diff→ empty- 3-layer compliance (
grep typer/click/formatter/httpx in wrong layer) → no hits; PR touches onlybuild/,.github/,docs/, and.gitignore— no Python source layer touched make check(withuv sync --extra serverfirst) → 4124 passed, 8 skipped, exit 0 ✓grep -E 'APPLE_TEAM_ID|46P6KJ65M2' diff→ hardcoded atrelease-kbagent.yml:203andsign_notarize.shcomment/example; non-secret public identifiers ✓ (NIT-3)grep 'DEB_KEY_PUBLIC' diff→ appears in ADR secrets table (line 958) but NOT in any workflow job env or script;index.sh:621explains it's derived — confirmed NB-3- ADR line 908 claims "macOS amd64/arm64"; freeze matrix line 168 is arm64 only — confirmed NB-2
grep '^+permissions:' diff→ top-levelcontents: write+id-token: writeat line 74-76 — confirmed NB-1; existingrelease.ymlalso uses top-level permissions (same pattern, same risk)- Conventional commit prefix:
feat(ci):— CI-only changes are oftenchore(ci):, butfeat:is acceptable for new infrastructure capability that delivers user-visible install paths; no rule violation - ADR status
Proposedatdocs/adr/0003-release-distribution-cicd.md:5; ADR 0001 and 0002 both sayAcceptedin this repo — confirmed NIT-1 - PyInstaller
--collect-all keboola_agent_clicorrectness:uv build --wheelruns the hatch build hook which populatessrc/keboola_agent_cli/_ui_dist/in-place; PyInstaller then collects it. Confirmed by readingscripts/hatch_build.py—_ui_dist/exists in the source tree after the hook runs ✓ - nfpm hash (
0a188f8b...) atpackage-linuxstep and jsign hash (05ca18d4...) atwindows/sign.sh:815are verified withsha256sum -c -before execution ✓ - wingetcreate Authenticode check (
Get-AuthenticodeSignatureassertsStatus -eq ValidandSubject -match Microsoft) before execution ✓ - Version regex tested locally: handles
0.44.0,0.44.0b1,0.44.0rc1,0.44.0.dev1correctly ✓ - Behavior verification: no CLI command changes in this PR; runtime behavior unchanged; no reproduction needed
Open questions for the author
-
The
test-installjob (line 392) has no explicitif:condition; it inherits skip-propagation frompublish-s3andhomebrew. Is this intentional, or would an explicitif: startsWith(github.ref, 'refs/tags/') && needs.version.outputs.IS_PRERELEASE == 'false'be clearer to future maintainers who wonder whytest-installnever appears in dry-run workflow dispatch runs? -
For the Windows signing step (
windows/sign.sh),KEYVAULT="${AZURE_KEYVAULT_NAME:-kbc-cli-code-signing}"andALIAS="${AZURE_CERT_ALIAS:-codesigning}"default to production Key Vault names. If a dev ever runs this script locally with the secrets env populated and forgets to overrideAZURE_KEYVAULT_NAME, they'll hit the production vault. Was this considered? (The script itself requires all threeWINDOWS_SIGNING_*secrets to be set and is only invoked from CI, so the practical risk is low — more of a "least-surprise" question.)
Default the workflow token to contents:read and grant elevated scopes only to the jobs that actually need them, per GitHub security-hardening guidance: - pypi, publish-s3: contents:read + id-token:write (OIDC for PyPI / AWS) - github-release: contents:write (softprops/action-gh-release) version, gate, freeze, package-linux and the external-PAT jobs (homebrew, chocolatey, winget) and test-install stay read-only. Previously every job inherited the workflow-level contents:write + id-token:write, so jobs that never publish (e.g. version, gate, freeze) ran with a token that could push to the repo and mint OIDC credentials.
|
Pushed The workflow token now defaults to
Note: |
…IT-1) - macOS build matrix is arm64-only (Apple Silicon). The ADR said "macOS amd64/arm64" but the freeze matrix has no Intel leg; Intel Macs fall back to `uv tool install`. (NB-2) - Drop DEB_KEY_PUBLIC from the secrets table: the apt keyring is exported inline from DEB_KEY_PRIVATE by index.sh and no DEB_KEY_PUBLIC is read anywhere in the scripts or workflow. RPM_KEY_PUBLIC stays — publish-s3 does pass it. (NB-3) - Status Proposed -> Accepted, matching the ADR 0001/0002 convention. (NIT-1)
|
Pushed
Remaining nits left for your call (policy / behavior trade-offs, not auto-applied): NIT-2 SHA-pin |
- NIT-2: SHA-pin cpina/github-action-push-to-another-repository to 07c4d7b (v1.7.2). It receives the write-scoped HOMEBREW_TAP_TOKEN, so a mutable tag re-point would be a supply-chain foothold. - NIT-3: move the non-secret Apple identifiers (APPLE_ACCOUNT_USERNAME, APPLE_TEAM_ID) from the freeze step env to the workflow-level env so rotation is a one-line change; the macOS sign step inherits them. - NIT-4: index.sh now fails loudly (exit 1) when APK_KEY_PRIVATE is set but abuild-sign/apk is unavailable, instead of silently skipping the apk index on a real release. Mirrors the existing DEB_KEY_PRIVATE guard; an unset APK_KEY_PRIVATE stays an explicit opt-out (warning + skip).
|
Pushed
Validated: workflow |
padak
left a comment
There was a problem hiding this comment.
All findings from the automated review pass are now addressed on this branch:
- NB-1 — per-job
GITHUB_TOKENscoping (least privilege) —16765c5 - NB-2 / NB-3 / NIT-1 — ADR drift (macOS arm64-only, drop unused
DEB_KEY_PUBLIC, statusAccepted) —c10ab06 - NIT-2 / NIT-3 / NIT-4 —
cpinaaction SHA-pinned, Apple identifiers moved to workflow-level env,index.shfails loudly whenAPK_KEY_PRIVATEis set but apk tooling is missing —9e08ad7
Workflow YAML and index.sh validated locally (yaml.safe_load, bash -n). Real jobs, correct tag gating, supply-chain-conscious. Approving. Squash as preferred on merge.
Summary
Ships kbagent as a self-contained native binary (no Python / no uv on the user's machine) packaged for brew / apt / dnf / apk / chocolatey / winget — the same install UX the legacy Go
kbcdelivered — under thekeboola-cli2package name so it coexists with the legacykbc.docs/adr/0003-release-distribution-cicd.md): the decision record — PyPI via OIDC Trusted Publishing, a PyInstaller freeze matrix (linux amd64/arm64, macOS arm64, windows amd64), sign/notarize, nfpm for deb/rpm/apk, Homebrew/Chocolatey/WinGet wrappers, publishing tocli-dist.keboola.com. Includes the required release secrets (where to get + how to set)..github/workflows/release-kbagent.yml): tag-triggered (v*.*.*), real (not stubbed). Jobs:version→gate→pypi/freeze(matrix) →package-linux→github-release/publish-s3→homebrew/chocolatey/winget→test-install.build/package/**(entry point, nfpm config, per-OS sign/notarize, repo indexing, Homebrew/Chocolatey templates) + asetup-buildcomposite action.Safety / gating
v*.*.*tag. Every external-publish job (pypi,publish-s3,homebrew,chocolatey,winget,github-release) is gated onstartsWith(github.ref, 'refs/tags/').workflow_dispatchis a DRY RUN: it freezes + signs + packages (exercising the build and signing creds) but publishes nothing externally.b/rc/.dev) build + package + GitHub pre-release only; all external publishing is skipped.releaseenvironment; thegatejob re-runs the per-PR checks against the tagged commit and fails fast if the tag version ≠pyproject.toml.keboola-as-coderepo.Coexistence with the legacy
kbcDistinct package name (
keboola-cli2), distinct S3 prefix (keboola-cli2/), distinct Homebrew tap (keboola/homebrew-keboola-cli2), distinct Chocolatey/WinGet ids — verified no collision with the existingkbcdistribution.Change type
CI + docs + packaging sources. No application/CLI source changes (the only Python added is a 3-line PyInstaller entry point). No version bump.
Test plan
env -i kbagent --version).release-kbagent.yml+setup-build/action.ymlparse as valid YAML;index.shpassesbash -n.test-install) installs via apt + Homebrew and assertskbagent --version.cli-dist-keboola-com/keboola-cli2/*,HOMEBREW_TAP_TOKENpush, first-time WinGet submission.Deployment
Merge is inert until a
v*.*.*tag is pushed and thereleaseenvironment + secrets are configured.Rollback plan
Revert of this PR; delete any published tag/release.
self-review skipped:
/kbagent:reviewrequires the kbagent Claude Code plugin; ran a thermo-nuclear + ponytail structural pass instead.