Publish from CI without a secret
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 17:15:58 +02:00
co-authored by Claude Opus 5
parent 2f725fdd14
commit 7026e0cc6a
4 changed files with 63 additions and 30 deletions
+3 -6
View File
@@ -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.
+26 -13
View File
@@ -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
+9 -5
View File
@@ -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
+25 -6
View File
@@ -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"