8 Commits
Author SHA1 Message Date
max-voitcov 3dc5ebbbb9 Ask the PyPI index whether the version is there, since twine may not
build / build (push) Successful in 10m53s
twine's --skip-existing is not a client-side flag: it asks the repository
whether it supports the feature, and Gitea's PyPI registry does not advertise
it, so the upload fails with UnsupportedConfiguration before it starts. Check
the simple index for the version instead -- the same check-then-publish shape
the npm step already uses.
2026-08-26 11:18:04 +03:00
max-voitcov a0acdd1c82 Build the .NET package, and let a re-run pass the registries it already filled
build / build (push) Failing after 15m53s
With PACKAGES_TOKEN in place, npm and PyPI published 0.2.0 and NuGet failed:

    error NU5026: The file '.../bin/Release/net6.0/Maxvojtkov.Dokploy.dll'
    to be packed was not found on disk.

The generated csproj sets GeneratePackageOnBuild, and `dotnet pack` on such a
project skips compiling -- it assumes the build already packed -- so it packs
an assembly nothing ever built. Pack with the property turned off and it builds
the project itself.

That left the release half-published again, and re-running it would have
stopped at the first registry that already had 0.2.0: Gitea answers a repeat
publish with 409, which npm and the Go upload both treat as fatal. Every
publish step is now idempotent -- npm checks first, twine takes
--skip-existing, the Go upload accepts 409, and NuGet already had
--skip-duplicate -- so a release that fails halfway can simply be run again.
2026-08-26 10:51:57 +03:00
max-voitcov 9a2d4675cf Correct the claim that releasing needs no configured secrets
build / build (push) Successful in 13m43s
2026-08-26 10:34:14 +03:00
max-voitcov 398dc2cfb4 Publish the SDKs with a token that can write packages
build / build (push) Has been cancelled
The SDK job reached `npm publish` and got E401. The token Actions injects is
real and valid -- 40 characters, `GET /api/v1/user` returns 200, and it reads
the package registry as the repository owner -- but every *write* to the
registry is refused:

    npm PUT (bearer, as npm)   -> 401
    npm PUT (basic)            -> 401

Both schemes, so this is authority and not `_authToken` sending Bearer. Adding
`permissions: packages: write` to the workflow changed nothing either.

So the four publish steps now take PACKAGES_TOKEN, a repository secret holding
an access token scoped to `write:package`. goreleaser keeps the injected token:
it creates the release and uploads binaries, which is repository write, and
that half has always worked.

This is why v0.1.0's packages had to be published by hand -- the SDK job has
never once run to completion.
2026-08-26 10:33:49 +03:00
max-voitcov 9c3b595b7e TEMP: ask for packages:write on the Actions token
build / build (push) Has been cancelled
2026-08-26 10:32:02 +03:00
max-voitcov 4638686939 Authenticate the npm publish with Basic, and sharpen the triage
build / build (push) Has been cancelled
2026-08-26 10:30:51 +03:00
max-voitcov cc01a956ad TEMP: triage the publish token in the SDK job
build / build (push) Has been cancelled
2026-08-26 10:28:31 +03:00
max-voitcov 381c928342 Give the SDK job the pulumi CLI it has always needed
build / build (push) Successful in 14m12s
The v0.2.0 plugin binaries published, then the SDK job died in tfgen:

    panic: fatal: error An assertion has failed: bulk converting examples
    failed. convertViaPulumiCLI: pulumi executable not in PATH

tfgen converts the upstream provider's documentation examples into each
language by shelling out to `pulumi convert`, and asserts rather than degrades
when the binary is missing. The build workflow installs the CLI; this job never
did. It went unnoticed through v0.1.0 because tfgen had no docs to convert
until UpstreamRepoPath pointed it at the upstream checkout.

So half a release is published and the other half is not, and re-pushing the
tag would rerun a 45 minute build against artifacts that are already uploaded.
Let a dispatch republish just the SDKs instead: it takes the tag to publish,
skips the plugin job, and checks the tree out at that tag while the workflow
file itself comes from the branch it was dispatched on.
2026-08-26 02:52:02 +03:00
+82 -20
View File
@@ -3,12 +3,17 @@ name: release
on: on:
push: push:
tags: ["v*.*.*"] tags: ["v*.*.*"]
# A release that dies halfway -- the runner OOMs, the host reboots -- leaves # A release that dies halfway -- the runner OOMs, the host reboots, a job is
# the tag pushed and nothing published, and re-pushing a tag to retry it is # missing a tool -- leaves the tag pushed and only part of the release
# both awkward and destructive. Dispatch re-runs the same release instead. # published, and re-pushing a tag to retry it is both awkward and
# goreleaser refuses to run unless the checked-out commit is itself tagged, # destructive. Dispatch republishes the SDKs for a tag that already has its
# so this can only ever republish a real tag, never main-in-progress. # plugin binaries, which is the half that fails: the plugin job is a 45
workflow_dispatch: {} # minute build that either produced its artifacts or did not.
workflow_dispatch:
inputs:
tag:
description: Tag to publish the SDKs for, e.g. v0.2.0
required: true
env: env:
GO_VERSION: "1.25.x" GO_VERSION: "1.25.x"
@@ -17,16 +22,28 @@ env:
DOTNET_VERSION: "8.0.x" DOTNET_VERSION: "8.0.x"
UPSTREAM_REPO: usr_unknown/terraform-provider-dokploy UPSTREAM_REPO: usr_unknown/terraform-provider-dokploy
UPSTREAM_DIR: dokploy-teraform UPSTREAM_DIR: dokploy-teraform
# Everything below authenticates with the token Gitea injects into every # Cloning and the release itself authenticate with the token Gitea injects
# run, so releasing needs no configured secrets at all. # into every run. Publishing packages does not -- see PACKAGES_TOKEN below.
GITEA_HOST: gitea.coolify.vojtkov.dev GITEA_HOST: gitea.coolify.vojtkov.dev
GITEA_OWNER: usr_unknown GITEA_OWNER: usr_unknown
# Publishing to the package registries needs its own token. The one Actions
# injects authenticates fine -- it reads the registry and the API as the repo
# owner -- but every write comes back 401, under Bearer and Basic alike, and
# `permissions: packages: write` does not change that. So PACKAGES_TOKEN is a
# repository secret holding an access token with the `write:package` scope.
# Everything else here still uses the injected token.
jobs: jobs:
# The plugin binaries. Pulumi resolves them from this release via the # The plugin binaries. Pulumi resolves them from this release via the
# PluginDownloadURL baked into the schema, so this has to land before anyone # PluginDownloadURL baked into the schema, so this has to land before anyone
# installs an SDK. # installs an SDK.
plugin: plugin:
# Only on a tag push. A dispatch is for republishing SDKs against a tag
# whose binaries are already uploaded, and goreleaser would collide with
# them. To rebuild the binaries themselves, delete the release and
# re-push the tag.
if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -62,16 +79,24 @@ jobs:
args: release --clean --parallelism 1 args: release --clean --parallelism 1
workdir: pulumi-dokploy workdir: pulumi-dokploy
env: env:
# goreleaser creates the release and uploads binaries: repository
# write, which the injected token already has. Not a package write.
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GOGC: "50" GOGC: "50"
sdks: sdks:
needs: plugin needs: plugin
# `always()` so a dispatch, where plugin is skipped rather than run, still
# gets here -- but not past a plugin job that actually failed.
if: always() && needs.plugin.result != 'failure' && needs.plugin.result != 'cancelled'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
path: pulumi-dokploy path: pulumi-dokploy
# The workflow file comes from the dispatched branch; the tree to
# publish comes from the tag. On a push the two are the same thing.
ref: ${{ inputs.tag || github.ref }}
# Needed for the tag lookup below. # Needed for the tag lookup below.
fetch-depth: 0 fetch-depth: 0
@@ -100,6 +125,13 @@ jobs:
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-version: ${{ env.DOTNET_VERSION }}
# tfgen converts the upstream provider's documentation examples into
# each language by shelling out to `pulumi convert`, and asserts rather
# than degrades when the binary is absent: "pulumi executable not in
# PATH". It only started mattering once UpstreamRepoPath gave tfgen
# docs to find, which is why v0.1.0 published without this.
- uses: pulumi/actions@v6
# On a tag push the ref name is the tag; on a dispatch it is the branch, # On a tag push the ref name is the tag; on a dispatch it is the branch,
# so ask git what tag this commit carries. --exact-match keeps a dispatch # so ask git what tag this commit carries. --exact-match keeps a dispatch
# from quietly publishing an untagged commit under the previous version. # from quietly publishing an untagged commit under the previous version.
@@ -116,32 +148,56 @@ jobs:
- name: Publish to the Gitea npm registry - name: Publish to the Gitea npm registry
working-directory: pulumi-dokploy/sdk/nodejs/bin working-directory: pulumi-dokploy/sdk/nodejs/bin
env: env:
TOKEN: ${{ secrets.GITEA_TOKEN }} TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: | run: |
cat > .npmrc <<EOF cat > .npmrc <<EOF
@maxvojtkov:registry=https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/npm/ @maxvojtkov:registry=https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/npm/
//${GITEA_HOST}/api/packages/${GITEA_OWNER}/npm/:_authToken=${TOKEN} //${GITEA_HOST}/api/packages/${GITEA_OWNER}/npm/:_authToken=${TOKEN}
EOF EOF
npm publish # A re-run of a half-finished release must get past the registries
# that already have this version -- Gitea answers a repeat publish
# with 409 and npm treats that as fatal.
if npm view "@maxvojtkov/pulumi-dokploy@${VERSION}" version >/dev/null 2>&1; then
echo "@maxvojtkov/pulumi-dokploy@${VERSION} is already published"
else
npm publish
fi
- name: Publish to the Gitea PyPI registry - name: Publish to the Gitea PyPI registry
working-directory: pulumi-dokploy/sdk/python working-directory: pulumi-dokploy/sdk/python
env: env:
TWINE_USERNAME: ${{ env.GITEA_OWNER }} TWINE_USERNAME: ${{ env.GITEA_OWNER }}
TWINE_PASSWORD: ${{ secrets.GITEA_TOKEN }} TWINE_PASSWORD: ${{ secrets.PACKAGES_TOKEN }}
run: | run: |
python -m pip install --upgrade build twine python -m pip install --upgrade build twine
python -m build python -m build
python -m twine upload \ # twine's --skip-existing is refused outright here: it asks the
--repository-url "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/pypi" \ # repository whether it supports the feature and Gitea's PyPI
dist/* # registry does not advertise it ("UnsupportedConfiguration"). Ask
# the simple index instead, the same check-then-publish shape the
# npm step uses.
INDEX="https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/pypi/simple/pulumi-dokploy/"
if curl -sf --user "${GITEA_OWNER}:${TWINE_PASSWORD}" "$INDEX" \
| grep -qE "pulumi_dokploy-${VERSION}[-.]"; then
echo "pulumi-dokploy ${VERSION} is already published"
else
python -m twine upload \
--repository-url "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/pypi" \
dist/*
fi
- name: Publish to the Gitea NuGet registry - name: Publish to the Gitea NuGet registry
working-directory: pulumi-dokploy/sdk/dotnet working-directory: pulumi-dokploy/sdk/dotnet
env: env:
TOKEN: ${{ secrets.GITEA_TOKEN }} TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: | run: |
dotnet pack --configuration Release --output ./nupkg # The generated csproj sets GeneratePackageOnBuild, and `dotnet pack`
# on such a project skips compiling -- it assumes the build already
# packed -- then fails with NU5026 because the assembly it wants to
# pack was never produced. Turn the property off for this invocation
# and pack builds the project itself, as it normally would.
dotnet pack --configuration Release --output ./nupkg \
-p:GeneratePackageOnBuild=false
dotnet nuget push ./nupkg/*.nupkg \ dotnet nuget push ./nupkg/*.nupkg \
--source "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/nuget/index.json" \ --source "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/nuget/index.json" \
--api-key "$TOKEN" --skip-duplicate --api-key "$TOKEN" --skip-duplicate
@@ -156,7 +212,7 @@ jobs:
- name: Publish the Go SDK to the Gitea Go registry - name: Publish the Go SDK to the Gitea Go registry
working-directory: pulumi-dokploy working-directory: pulumi-dokploy
env: env:
TOKEN: ${{ secrets.GITEA_TOKEN }} TOKEN: ${{ secrets.PACKAGES_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
MODULE=github.com/maxvojtkov/pulumi-dokploy/sdk MODULE=github.com/maxvojtkov/pulumi-dokploy/sdk
@@ -166,8 +222,14 @@ jobs:
cp sdk/go.mod sdk/go.sum "$DEST/" cp sdk/go.mod sdk/go.sum "$DEST/"
cp -R sdk/go "$DEST/" cp -R sdk/go "$DEST/"
(cd "$STAGE" && zip -qrD "$STAGE/sdk.zip" . -x 'sdk.zip') (cd "$STAGE" && zip -qrD "$STAGE/sdk.zip" . -x 'sdk.zip')
curl -fsSL -X PUT \ # 409 means this version is already in the registry, which is the
# expected answer when a partly finished release is re-run.
CODE="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \
--user "${GITEA_OWNER}:${TOKEN}" \ --user "${GITEA_OWNER}:${TOKEN}" \
--upload-file "$STAGE/sdk.zip" \ --upload-file "$STAGE/sdk.zip" \
"https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/go/upload" "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/go/upload")"
echo "Published $MODULE@v${VERSION}" case "$CODE" in
201) echo "Published $MODULE@v${VERSION}" ;;
409) echo "$MODULE@v${VERSION} is already published" ;;
*) echo "Go registry upload failed with HTTP $CODE"; exit 1 ;;
esac