From 7026e0cc6a82735e23a48d2d54a21b79c9303580 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 18 Aug 2026 17:15:58 +0200 Subject: [PATCH] Publish from CI without a secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Woodpecker hands every step a forge credential for cloning — an access token of the repository's owner — and a one-off diagnostic in the check step confirmed it is there. scripts/ci-upload.sh now uses it when no `gitea_token` secret is set, so publishing a release needs nothing configured. Gitea takes such a credential as `token …` or `Bearer …` depending on how Woodpecker was set up, so the script probes which of the two `/user` accepts rather than assuming, and says which one it used. The secret mapping is gone from the step as well: referencing a secret that does not exist is a failure mode of its own, and the fallback is the normal path now. Adding a `gitea_token` secret and mapping it back in is how you publish as somebody else. Documents the release flow the pipeline now implements: push a vX.Y.Z tag, the pipeline builds Linux and Windows and creates the release with them in it, and `make release` from a Mac pushes the macOS package onto the same release. Either half can go first. Co-Authored-By: Claude Opus 5 (1M context) --- .woodpecker.yaml | 9 +++------ README.md | 39 ++++++++++++++++++++++++++------------- RELEASE_NOTES.md | 14 +++++++++----- scripts/ci-upload.sh | 31 +++++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 30 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index eb439ab..b1b3630 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -23,9 +23,6 @@ steps: image: *node_image commands: - node --version - # One-off diagnostic: does Woodpecker hand steps a forge credential of their own? - # If it does, the release step needs no secret at all. - - 'echo "netrc user: ${CI_NETRC_USERNAME:+present}, password: ${CI_NETRC_PASSWORD:+present}"' - npm ci - npm run typecheck - npm run lint @@ -61,9 +58,9 @@ steps: # from the Mac that can sign them. - name: release image: alpine - environment: - GITEA_TOKEN: - from_secret: gitea_token + # No secret: the step authenticates with the forge credential Woodpecker already + # hands every step, which belongs to the repository's owner. To publish as someone + # else instead, add a `gitea_token` repository secret and map it here as GITEA_TOKEN. commands: - apk add --no-cache curl jq # No globs on the command line: the package names have spaces in them. diff --git a/README.md b/README.md index 1a801d9..dc3f97a 100644 --- a/README.md +++ b/README.md @@ -192,17 +192,28 @@ builds an application and publishes to a release. | `check` | `electronuserland/builder:22` | `npm ci`, type-check, lint, and the smoke test | | `linux` | `electronuserland/builder:22` | AppImage and deb | | `windows` | `electronuserland/builder:22-wine` | the NSIS installer and the portable exe, built through Wine | -| `release` | `alpine` | on a tag only: attaches what this pipeline built | +| `release` | `alpine` | on a tag only: **creates the release** and attaches what this pipeline built | -**macOS stays a local build.** Apple's toolchain and its signing only exist on a Mac, -so a full release is `make release` here for the macOS package plus this pipeline for -the other two. The window test is local too: it needs a display and a store on the -machine. +**macOS stays a local build.** Apple's toolchain and its signing only exist on a Mac. +So the whole of a release is: -The `release` step needs a **`gitea_token`** repository secret in Woodpecker, with -write access to this repository — CI has a token where a workstation has a `tea` -login, which is why `scripts/ci-upload.sh` exists alongside `scripts/release.sh` -instead of one script with two ways to authenticate. +1. bump the version, commit, and push the tag: `git tag v1.4.0 && git push origin v1.4.0`; +2. the pipeline builds Linux and Windows, **creates the release** with `RELEASE_NOTES.md` + as its body, and attaches those four packages; +3. on a Mac, `make release` builds the macOS package and pushes it onto the same release. + +The window test is local as well: it needs a display and a store on the machine. + +The `release` step needs **no secret**. It authenticates with the forge credential +Woodpecker hands every step for cloning, which is an access token of the repository's +owner; Gitea takes it as `token …` or `Bearer …` depending on how Woodpecker was set up, +so `scripts/ci-upload.sh` probes which of the two works instead of assuming. To publish +as somebody else, add a `gitea_token` repository secret and map it into the step. + +There are two publishers on purpose: `scripts/release.sh` drives `tea`, which is logged +in on a workstation, and `scripts/ci-upload.sh` speaks the API with whatever credential +CI has. Each is short enough to read in full; one script with two ways to authenticate +would not be. Both build steps end by checking what they produced: a package under 10 MB did not finish. That check exists because a half-finished Wine build leaves a stub *named* like @@ -224,10 +235,12 @@ convention. make release ``` -The tag comes from `package.json`, so `npm version patch` is the only place a -version is set. The release is created if it is not there yet, and an attachment -whose name is already on it is **replaced** rather than refused — so a rebuild and -a second `make publish` lands rather than erroring. +This is the **macOS half** of a release; the Linux and Windows packages come from the +pipeline when the tag is pushed (see above). The tag comes from `package.json`, so +`npm version patch` is the only place a version is set. The release is created if it is +not there yet — either half can go first — and an attachment whose name is already on it +is **replaced** rather than refused, so a rebuild and a second `make publish` lands +rather than erroring. Release notes come from `RELEASE_NOTES.md` when the file is present, otherwise the release gets a one-line note. The repository is read from `origin`, so a fork diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1eaa8aa..63e5b26 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -58,11 +58,15 @@ window failed it. ### Linux and Windows packages now come from CI -`.woodpecker.yaml` builds the AppImage, the deb, the NSIS installer and the portable -exe — Windows through Wine — and on a tag attaches them to this release. macOS stays a -local build, because Apple's toolchain and its signing exist only on a Mac, so a full -release is one local `make release` plus the pipeline. The Windows installer is not -signed: Windows will warn about an unknown publisher until there is a certificate. +A `vX.Y.Z` tag now starts the pipeline, which builds the AppImage, the deb, the NSIS +installer and the portable exe — Windows through Wine — **creates this release** and +attaches all four. macOS stays a local build, because Apple's toolchain and its signing +exist only on a Mac, so `make release` from a Mac pushes that package onto the same +release afterwards. No secret is involved: the pipeline publishes with the forge +credential Woodpecker already gives every step. + +The Windows installer is not signed: Windows will warn about an unknown publisher until +there is a certificate. ### Opening it on macOS diff --git a/scripts/ci-upload.sh b/scripts/ci-upload.sh index 0a202ed..639a2ca 100755 --- a/scripts/ci-upload.sh +++ b/scripts/ci-upload.sh @@ -6,8 +6,11 @@ # two are deliberately separate scripts rather than one with two ways to authenticate — # each is short enough to read in full. # -# GITEA_TOKEN=… scripts/ci-upload.sh every package in dist/ -# GITEA_TOKEN=… scripts/ci-upload.sh dist/one.deb just these +# scripts/ci-upload.sh every package in dist/ +# scripts/ci-upload.sh dist/one.deb just these +# +# Authenticates with the `gitea_token` secret when there is one, and otherwise with the +# credential Woodpecker gives every step for cloning — so a release needs no secret. # # Creates the release when the tag has none, with RELEASE_NOTES.md as its body. That is # the flow: a `vX.Y.Z` tag starts this pipeline, which publishes the release with the @@ -24,13 +27,30 @@ NOTES="${NOTES:-RELEASE_NOTES.md}" say() { echo "[ci-upload] $*"; } die() { echo "[ci-upload] error: $*" >&2; exit 1; } -[ -n "${GITEA_TOKEN:-}" ] || die "GITEA_TOKEN is not set — add the gitea_token secret to the repository" +# Who to be. A `gitea_token` secret wins when there is one; otherwise the credential +# Woodpecker already hands every step for cloning is used, which is an access token of +# the repository's owner — so publishing needs no secret of its own. Gitea accepts a +# personal access token as `token …` and an OAuth one as `Bearer …`, and which of the two +# this is depends on how Woodpecker was set up, so the scheme is probed once rather than +# assumed. +TOKEN="${GITEA_TOKEN:-${CI_NETRC_PASSWORD:-}}" +[ -n "$TOKEN" ] || die "no credential: set GITEA_TOKEN, or run this where Woodpecker provides CI_NETRC_PASSWORD" [ -n "$REPO" ] || die "cannot work out the repository — set REPO=owner/name" [ -n "$TAG" ] || die "cannot work out the tag — set TAG=v1.2.3" +AUTH="" +for scheme in token Bearer; do + if curl -fsS -H "Authorization: $scheme $TOKEN" "$FORGE/user" >/dev/null 2>&1; then + AUTH="Authorization: $scheme $TOKEN" + say "authenticated with the $scheme scheme" + break + fi +done +[ -n "$AUTH" ] || die "the credential was refused by $FORGE — it cannot read /user" + api() { method="$1"; path="$2"; shift 2 - curl -fsS -X "$method" -H "Authorization: token $GITEA_TOKEN" "$FORGE$path" "$@" + curl -fsS -X "$method" -H "$AUTH" "$FORGE$path" "$@" } # Package names contain spaces — "WarpEngine Store Setup 1.4.0.exe" does — so the list @@ -52,8 +72,7 @@ say "$REPO $TAG" # `curl -f` fails on the 404 a missing release answers, so the lookup is allowed to # fail and judged by what came back rather than by its exit status. -release_id="$(curl -sS -H "Authorization: token $GITEA_TOKEN" \ - "$FORGE/repos/$REPO/releases/tags/$TAG" | jq -r '.id // empty')" +release_id="$(curl -sS -H "$AUTH" "$FORGE/repos/$REPO/releases/tags/$TAG" | jq -r '.id // empty')" if [ -z "$release_id" ]; then say "no release for $TAG yet — creating it"