name: release on: push: tags: ["v*.*.*"] # A release that dies halfway -- the runner OOMs, the host reboots, a job is # missing a tool -- leaves the tag pushed and only part of the release # published, and re-pushing a tag to retry it is both awkward and # destructive. Dispatch republishes the SDKs for a tag that already has its # plugin binaries, which is the half that fails: the plugin job is a 45 # 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: GO_VERSION: "1.25.x" NODE_VERSION: "20.x" PYTHON_VERSION: "3.11" DOTNET_VERSION: "8.0.x" UPSTREAM_REPO: usr_unknown/terraform-provider-dokploy UPSTREAM_DIR: dokploy-teraform # Cloning and the release itself authenticate with the token Gitea injects # into every run. Publishing packages does not -- see PACKAGES_TOKEN below. GITEA_HOST: gitea.coolify.vojtkov.dev 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: # The plugin binaries. Pulumi resolves them from this release via the # PluginDownloadURL baked into the schema, so this has to land before anyone # installs an SDK. 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 steps: - uses: actions/checkout@v4 with: path: pulumi-dokploy fetch-depth: 0 - uses: actions/checkout@v4 with: repository: ${{ env.UPSTREAM_REPO }} path: ${{ env.UPSTREAM_DIR }} token: ${{ secrets.GITEA_TOKEN }} # checkout's default-branch lookup returns "not found" on this Gitea. ref: main - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false # A bridged Pulumi provider links the whole Terraform provider plus the # Pulumi SDK into one ~100MB binary, and compiling that is memory-hungry. # goreleaser defaults its parallelism to the CPU count, so several of # those compiles overlap and the runner OOMs -- the v0.2.0 release died # after 31 minutes with `compile: signal: killed` on darwin_amd64. # # Build one target at a time. It is slower in wall-clock but it is the # difference between a release that finishes and one that does not. # GOGC trades some CPU for a lower peak heap in the compiler itself. - uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean --parallelism 1 workdir: pulumi-dokploy 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 }} GOGC: "50" sdks: 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 steps: - uses: actions/checkout@v4 with: 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. fetch-depth: 0 - uses: actions/checkout@v4 with: repository: ${{ env.UPSTREAM_REPO }} path: ${{ env.UPSTREAM_DIR }} token: ${{ secrets.GITEA_TOKEN }} # checkout's default-branch lookup returns "not found" on this Gitea. ref: main - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: actions/setup-dotnet@v4 with: 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, # so ask git what tag this commit carries. --exact-match keeps a dispatch # from quietly publishing an untagged commit under the previous version. - name: Derive version from tag working-directory: pulumi-dokploy run: | TAG="$(git describe --tags --exact-match)" echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" - name: Build SDKs working-directory: pulumi-dokploy run: make build_sdks VERSION=${{ env.VERSION }} - name: Publish to the Gitea npm registry working-directory: pulumi-dokploy/sdk/nodejs/bin env: TOKEN: ${{ secrets.PACKAGES_TOKEN }} run: | cat > .npmrc </dev/null 2>&1; then echo "@maxvojtkov/pulumi-dokploy@${VERSION} is already published" else npm publish fi - name: Publish to the Gitea PyPI registry working-directory: pulumi-dokploy/sdk/python env: TWINE_USERNAME: ${{ env.GITEA_OWNER }} TWINE_PASSWORD: ${{ secrets.PACKAGES_TOKEN }} run: | python -m pip install --upgrade build twine python -m build # twine's --skip-existing is refused outright here: it asks the # repository whether it supports the feature and Gitea's PyPI # 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 working-directory: pulumi-dokploy/sdk/dotnet env: TOKEN: ${{ secrets.PACKAGES_TOKEN }} run: | # 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 \ --source "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/nuget/index.json" \ --api-key "$TOKEN" --skip-duplicate # The Go SDK's module path stays github.com/maxvojtkov/..., which no # amount of Gitea hosting changes. Uploading it to Gitea's Go registry is # what makes that path resolvable anyway: consumers point GOPROXY here. # # A module zip must have every entry prefixed `@/` and # must contain no directory entries at all -- hence `zip -D`, without # which `go get` fails with "has unexpected file". - name: Publish the Go SDK to the Gitea Go registry working-directory: pulumi-dokploy env: TOKEN: ${{ secrets.PACKAGES_TOKEN }} run: | set -euo pipefail MODULE=github.com/maxvojtkov/pulumi-dokploy/sdk STAGE="$(mktemp -d)" DEST="$STAGE/$MODULE@v${VERSION}" mkdir -p "$DEST" cp sdk/go.mod sdk/go.sum "$DEST/" cp -R sdk/go "$DEST/" (cd "$STAGE" && zip -qrD "$STAGE/sdk.zip" . -x 'sdk.zip') # 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}" \ --upload-file "$STAGE/sdk.zip" \ "https://${GITEA_HOST}/api/packages/${GITEA_OWNER}/go/upload")" 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