diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..de335ce --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,105 @@ +name: Build and push docker image + +on: + workflow_dispatch: + push: + branches: [main] + +env: + CARGO_TERM_COLOR: always + REGISTRY_IMAGE: ghcr.io/svix/openapi-codegen + +jobs: + build: + strategy: + matrix: + platform: + - runner: ubuntu-24.04 + name: amd64 + - runner: ubuntu-24.04-arm + name: arm64 + name: Build and publish ${{ matrix.platform.name }} docker image + if: github.ref == 'refs/heads/main' + runs-on: "${{ matrix.platform.runner }}" + steps: + - uses: actions/checkout@v4 + + - name: Login to ghcr + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + tags: ${{ env.REGISTRY_IMAGE }} + file: Dockerfile.${{ matrix.platform.name }} + cache-from: type=gha + cache-to: type=gha + platforms: linux/${{ matrix.platform.name }} + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + # we create empty files with the sha256 digest of the docker image as the filename + # since we did not push with a tag, the only way to identify the image is with the digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.platform.name }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + publish-merged-manifest: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + needs: + - build + steps: + - uses: actions/checkout@v4 + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Login to ghcr + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - run: echo "IMAGE_TAG=$(date +%Y%m%d)-$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" + + - name: Create manifest list and push + # inside the ${{ runner.temp }}/digests we downloaded empty files with the sha256 digest of the image as the filename + # using printf we get the digest from the filename and we add the digest to the manifest + # this is the recommend way of doing things :( + # https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY_IMAGE }}:latest \ + -t ${{ env.REGISTRY_IMAGE }}:${{ env.IMAGE_TAG }} \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect "${{ env.REGISTRY_IMAGE }}:latest" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1005b05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,102 @@ +# build openapi-codegen +FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.85 AS chef +WORKDIR /app + +FROM chef AS planner + +COPY Cargo.toml . +COPY Cargo.lock . +COPY build.rs . +COPY src /app/src + +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS openapi-codegen-builder + +COPY --from=planner /app/recipe.json recipe.json + +RUN cargo chef cook --release --recipe-path recipe.json + +COPY Cargo.toml . +COPY Cargo.lock . +COPY build.rs . +COPY src /app/src + +RUN cargo build --release --bin openapi-codegen + +# build goimports +FROM docker.io/golang:1.24-bookworm AS goimports-builder +RUN go install golang.org/x/tools/cmd/goimports@latest + +# build rubyfmt +FROM docker.io/rust:1.85 AS rubyfmt-builder +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ruby bison && \ + apt-get clean + +RUN git clone https://github.com/fables-tales/rubyfmt.git \ + --recurse-submodules --shallow-submodules /app && \ + git checkout 71cbb4adc53d3d8b36a6f1b3dcff87865d0204b8 + +RUN cargo build --release + +# main container +FROM docker.io/ubuntu:noble + +ENV DEBIAN_FRONTEND=noninteractive +ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.cargo/bin/:/root/.dotnet/tools" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl default-jre-headless dotnet8 && \ + apt-get clean + + +# C# +RUN dotnet tool install csharpier --version 0.30.6 -g + +# # Rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ + -y \ + --profile minimal \ + --no-modify-path \ + --no-update-default-toolchain \ + --default-toolchain nightly-2025-02-27 \ + --component rustfmt + + +# Javascript +COPY --from=ghcr.io/biomejs/biome:1.9.4 /usr/local/bin/biome /usr/bin/biome + +# Python +COPY --from=ghcr.io/astral-sh/ruff:0.9.8 /ruff /usr/bin/ruff + +# Java +RUN echo "25157797a0a972c2290b5bc71530c4f7ad646458025e3484412a6e5a9b8c9aa6 google-java-format-1.25.2-all-deps.jar" > google-java-format-1.25.2-all-deps.jar.sha256 && \ + curl -fsSL --output google-java-format-1.25.2-all-deps.jar "https://github.com/google/google-java-format/releases/download/v1.25.2/google-java-format-1.25.2-all-deps.jar" && \ + sha256sum google-java-format-1.25.2-all-deps.jar.sha256 -c && \ + rm google-java-format-1.25.2-all-deps.jar.sha256 && \ + mv google-java-format-1.25.2-all-deps.jar /usr/bin/ && \ + echo '#!/usr/bin/bash\njava -jar /usr/bin/google-java-format-1.25.2-all-deps.jar $@' > /usr/bin/google-java-format && \ + chmod +x /usr/bin/google-java-format + +# Kotlin +RUN echo "5e7eb28a0b2006d1cefbc9213bfc73a8191ec2f85d639ec4fc4ec0cd04212e82 ktfmt-0.54-jar-with-dependencies.jar" > ktfmt-0.54-jar-with-dependencies.jar.sha256 && \ + curl -fsSL --output ktfmt-0.54-jar-with-dependencies.jar "https://github.com/facebook/ktfmt/releases/download/v0.54/ktfmt-0.54-jar-with-dependencies.jar" && \ + sha256sum ktfmt-0.54-jar-with-dependencies.jar.sha256 -c && \ + rm ktfmt-0.54-jar-with-dependencies.jar.sha256 && \ + mv ktfmt-0.54-jar-with-dependencies.jar /usr/bin/ && \ + echo '#!/usr/bin/bash\njava -jar /usr/bin/ktfmt-0.54-jar-with-dependencies.jar $@' > /usr/bin/ktfmt && \ + chmod +x /usr/bin/ktfmt + +# Go +COPY --from=goimports-builder /go/bin/goimports /usr/bin +COPY --from=goimports-builder /usr/local/go/bin/gofmt /usr/bin + +# openapi-codegen +COPY --from=openapi-codegen-builder /app/target/release/openapi-codegen /usr/bin/ + +# Ruby +COPY --from=rubyfmt-builder /app/target/release/rubyfmt-main /usr/bin/rubyfmt + diff --git a/src/generator.rs b/src/generator.rs index 097e2ff..991c27d 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -12,6 +12,7 @@ use crate::{ postprocessing::Postprocessor, template, types::Types, + GenerateFlags, }; #[derive(Default, Deserialize)] @@ -29,7 +30,7 @@ pub(crate) fn generate( types: Types, tpl_name: String, output_dir: &Utf8Path, - no_postprocess: bool, + flags: GenerateFlags, ) -> anyhow::Result<()> { let (name_without_jinja_suffix, tpl_path) = match tpl_name.strip_suffix(".jinja") { Some(basename) => (basename, &tpl_name), @@ -63,6 +64,8 @@ pub(crate) fn generate( minijinja_env.add_template(tpl_path, &tpl_source)?; let tpl = minijinja_env.get_template(tpl_path)?; + fs::create_dir_all(output_dir)?; + let postprocessor = Postprocessor::from_ext(tpl_file_ext, output_dir); let generator = Generator { @@ -70,7 +73,7 @@ pub(crate) fn generate( tpl_file_ext, output_dir, postprocessor: &postprocessor, - no_postprocess, + flags, }; match tpl_kind { @@ -80,7 +83,7 @@ pub(crate) fn generate( TemplateKind::Summary => generator.generate_summary(types, api)?, } - if !no_postprocess { + if !flags.no_postprocess { postprocessor.run_postprocessor(); } @@ -92,7 +95,7 @@ struct Generator<'a> { tpl_file_ext: &'a str, output_dir: &'a Utf8Path, postprocessor: &'a Postprocessor, - no_postprocess: bool, + flags: GenerateFlags, } impl Generator<'_> { @@ -172,11 +175,12 @@ impl Generator<'_> { }; let file_path = self.output_dir.join(format!("{basename}.{tpl_file_ext}")); + let out_file = BufWriter::new(File::create(&file_path)?); self.tpl.render_to_write(ctx, out_file)?; - if !self.no_postprocess { + if !self.flags.no_postprocess { self.postprocessor.add_path(&file_path); } diff --git a/src/main.rs b/src/main.rs index 710fdf7..a338357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,7 +141,7 @@ fn analyze_and_generate( writeln!(types_file, "{types:#?}")?; } - generate(api, types, template, path, flags.no_postprocess)?; + generate(api, types, template, path, flags)?; } println!("done! output written to {path}"); diff --git a/src/postprocessing.rs b/src/postprocessing.rs index 78da208..905f1b2 100644 --- a/src/postprocessing.rs +++ b/src/postprocessing.rs @@ -92,7 +92,7 @@ impl PostprocessorLanguage { Self::Rust => &[( "rustfmt", &[ - "+nightly", + "+nightly-2025-02-27", "--unstable-features", "--skip-children", "--edition",