Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/publish-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Publish a release Docker image to GHCR.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job
# within `cargo-dist`. Runs after `host`, so the GitHub Release and its binary artifacts exist;
# the Dockerfile fetches the prebuilt musl binary from the release at image-build time.

name: "[tower] Publish Docker image"

on:
workflow_call:
inputs:
plan:
required: true
type: string

jobs:
docker-publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
env:
TAG: ${{ fromJson(inputs.plan).announcement_tag }}
PRERELEASE: ${{ fromJson(inputs.plan).announcement_is_prerelease }}
run: |
VERSION="${TAG#v}"
TAGS=(--tag "ghcr.io/tower/tower-cli:$VERSION")
if [ "$PRERELEASE" != "true" ]; then
TAGS+=(--tag "ghcr.io/tower/tower-cli:latest")
fi
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg VERSION="$VERSION" \
"${TAGS[@]}" \
Comment on lines +35 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In cargo-dist release manifests, is announcement_tagguaranteed to include avprefix, or can it be plain semver like1.2.3?

💡 Result:

In cargo-dist release manifests, announcement_tag is not guaranteed to include a v prefix and can be plain semver like 1.2.3. The field announcement_tag in DistManifest is an Option described as "The (git) tag associated with this announcement". Cargo-dist's tag parsing is permissive: it matches git tags using the regex ^[0-9]+.[0-9]+.[0-9]+*, which captures plain semver versions like 1.2.3 without a v prefix. Official docs and examples emphasize conventional formats like v1.0.0 or my-app-v1.0.0, but a key PR (#346) explicitly made parsing more flexible: "The 'v' prefix on a version is now optional". Supported formats include: - v1.0.0 - 1.0.0 - package-name-v1.0.0 - package-name-1.0.0 - package-name/v1.0.0 - package-name/1.0.0 Thus, plain semver like 1.2.3 is valid, and announcement_tag will reflect whatever git tag triggered the release or was passed via --tag.

Citations:


🏁 Script executed:

# First, find and read the Dockerfile to see the release URL construction
fd -t f "Dockerfile" | head -20

Repository: tower/tower-cli

Length of output: 69


🏁 Script executed:

# Check recent git tags to see what format the project uses
git tag -l | head -20

Repository: tower/tower-cli

Length of output: 41


🏁 Script executed:

# Read the Dockerfile to inspect the release URL construction
fd -t f "Dockerfile" -x cat -n

Repository: tower/tower-cli

Length of output: 851


🏁 Script executed:

cat -n .github/workflows/publish-docker.yml | sed -n '30,50p'

Repository: tower/tower-cli

Length of output: 739


🏁 Script executed:

git tag --list | tail -20

Repository: tower/tower-cli

Length of output: 41


Avoid v stripping/reconstruction mismatch for release asset lookup.

Line 35 strips v from TAG (the announcement_tag from cargo-dist), while the Dockerfile hardcodes v${VERSION} in the release download URL (Dockerfile line 11). Since cargo-dist allows plain semver tags like 1.2.3 (without v prefix), a release created with a non-prefixed tag would cause the Docker build to fail—it would request /download/v1.2.3/ when assets are actually at /download/1.2.3/.

Pass the original tag value to the Dockerfile so it uses the exact release tag format:

Suggested fix
-          VERSION="${TAG#v}"
+          VERSION="${TAG#v}"
+          RELEASE_TAG="$TAG"
           TAGS=(--tag "ghcr.io/tower/tower-cli:$VERSION")
@@
           docker buildx build \
             --platform linux/amd64,linux/arm64 \
             --build-arg VERSION="$VERSION" \
+            --build-arg RELEASE_TAG="$RELEASE_TAG" \
             "${TAGS[@]}" \
             --push .

In Dockerfile:

-ARG VERSION
+ARG VERSION
+ARG RELEASE_TAG
@@
-      "https://github.com/tower/tower-cli/releases/download/v${VERSION}/tower-${ARCH}-unknown-linux-musl.tar.xz" \
+      "https://github.com/tower/tower-cli/releases/download/${RELEASE_TAG}/tower-${ARCH}-unknown-linux-musl.tar.xz" \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/publish-docker.yml around lines 35 - 43, The build strips
a leading "v" into VERSION but the Dockerfile expects the original tag
(v-prefixed or not) when constructing the release download URL, causing
mismatches; stop relying on stripped VERSION for the download and pass the
original TAG through to the Docker build. Modify the workflow to set
VERSION="${TAG#v}" as before for image tagging but also pass the unmodified TAG
as a separate build-arg (e.g., RELEASE_TAG or FULL_TAG) in the docker buildx
build invocation (alongside --build-arg VERSION="$VERSION"), and update the
Dockerfile to use that RELEASE_TAG when forming the /download/... URL (instead
of v${VERSION}) so the exact release tag format is preserved.

--push .
23 changes: 9 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,20 +312,15 @@ jobs:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

announce:
custom-publish-docker:
needs:
- plan
- host
- custom-publish-pypi
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-docker.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
permissions:
"contents": "read"
"packages": "write"
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM --platform=$BUILDPLATFORM alpine:3 AS fetch
ARG TARGETARCH
ARG VERSION
RUN apk add --no-cache curl tar xz \
&& case "$TARGETARCH" in \
amd64) ARCH=x86_64 ;; \
arm64) ARCH=aarch64 ;; \
*) echo "unsupported TARGETARCH: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& curl -fsSL -o /tmp/tower.tar.xz \
"https://github.com/tower/tower-cli/releases/download/v${VERSION}/tower-${ARCH}-unknown-linux-musl.tar.xz" \
&& mkdir -p /out \
&& tar -xJf /tmp/tower.tar.xz -C /out --strip-components=1 \
&& chmod +x /out/tower

FROM gcr.io/distroless/static-debian12
COPY --from=fetch /out/tower /usr/local/bin/tower
ENTRYPOINT ["/usr/local/bin/tower"]
Comment on lines +16 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does gcr.io/distroless/static-debian12 run as root by default, and what is the recommended non-root UID/GID for distroless static images?

💡 Result:

Yes, gcr.io/distroless/static-debian12 (the latest tag) runs as root (UID 0) by default. The image has variants including :nonroot, which sets the default user to the nonroot user with UID/GID 65532. The recommended non-root UID/GID for distroless static images is 65532:65532, corresponding to the built-in nonroot user. This is the standard unprivileged user provided in all distroless images for running containers securely without root privileges. Users can also use nobody (UID/GID 65534), but nonroot is the specifically recommended one via the :nonroot tag. To use it, either pull gcr.io/distroless/static-debian12:nonroot or add USER nonroot (or USER 65532:65532) in your Dockerfile when basing on the root variant.

Citations:


Run the container as non-root.

Line 16 uses gcr.io/distroless/static-debian12, which defaults to running as root (UID 0). Lines 17–18 do not set a user, so the container will run with root privileges. Add a USER directive in the final stage to run as the recommended non-root user.

Suggested fix
 FROM gcr.io/distroless/static-debian12
 COPY --from=fetch /out/tower /usr/local/bin/tower
+USER 65532:65532
 ENTRYPOINT ["/usr/local/bin/tower"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM gcr.io/distroless/static-debian12
COPY --from=fetch /out/tower /usr/local/bin/tower
ENTRYPOINT ["/usr/local/bin/tower"]
FROM gcr.io/distroless/static-debian12
COPY --from=fetch /out/tower /usr/local/bin/tower
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/tower"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 16 - 18, The final image stage uses
gcr.io/distroless/static-debian12 and sets ENTRYPOINT to /usr/local/bin/tower
but does not set a non-root user; update the final stage of the Dockerfile to
add a USER directive (e.g., a non-root UID/GID like 1000 or a created user) so
the container does not run as root, and ensure /usr/local/bin/tower and any
required directories have appropriate ownership/permissions for that UID before
switching users (adjust COPY/permission steps as needed).

4 changes: 2 additions & 2 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ installers = ["shell", "homebrew", "msi"]
# A GitHub repo to push Homebrew formulas to
tap = "tower/tower-cli"
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
# Path that installers should place binaries in
install-path = "CARGO_HOME"
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi"]
publish-jobs = ["./publish-pypi", "./publish-docker"]
# Whether to install an updater program
install-updater = false
# Whether dist should create a Github Release or use an existing draft
Expand Down
Loading