<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Piotr Janik</title>
    <description>The latest articles on DEV Community by Piotr Janik (@piotrjanik).</description>
    <link>https://dev.to/piotrjanik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1524139%2F9320e285-a15e-4f30-948c-ef2bc7836f63.jpg</url>
      <title>DEV Community: Piotr Janik</title>
      <link>https://dev.to/piotrjanik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/piotrjanik"/>
    <language>en</language>
    <item>
      <title>From git push to Fine-Tuned Model in Production</title>
      <dc:creator>Piotr Janik</dc:creator>
      <pubDate>Mon, 13 Jul 2026 22:03:05 +0000</pubDate>
      <link>https://dev.to/piotrjanik/from-git-push-to-fine-tuned-model-in-production-2ghn</link>
      <guid>https://dev.to/piotrjanik/from-git-push-to-fine-tuned-model-in-production-2ghn</guid>
      <description>&lt;p&gt;At some point in every engineer's life, they sit down and think: &lt;em&gt;"You know what this fine-tuning pipeline needs? More YAML."&lt;/em&gt; That moment arrived for me, and the result is &lt;a href="https://github.com/piotrjanik/nebius-actions" rel="noopener noreferrer"&gt;&lt;code&gt;nebius-actions&lt;/code&gt;&lt;/a&gt; — a set of composable GitHub Actions that let you train, deploy, and smoke-test a model on &lt;a href="https://nebius.com/" rel="noopener noreferrer"&gt;Nebius AI Cloud&lt;/a&gt; GPU infrastructure, entirely from a GitHub workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I wanted one thing: click a button in GitHub, have a real GPU spin up, fine-tune a model, package it into a serving image, deploy it to a live endpoint, prove it works, and clean everything up — with zero manual steps and no leaked cloud resources. No Jupyter notebooks saved as "final_v3_REAL.ipynb". No SSH-ing into a VM to babysit training. Just a workflow that does the whole thing and gets out of your way.&lt;/p&gt;

&lt;p&gt;The result is demo-run-job.yml: it QLoRA-fine-tunes Qwen2.5-0.5B on wikitext for 30 steps with Axolotl, bakes the LoRA adapters into a vLLM serving image, deploys it to a Nebius Endpoint, and smoke-tests it — all triggered by a single workflow_dispatch click.&lt;/p&gt;

&lt;p&gt;The pipeline runs across five GitHub jobs: submit, wait, deploy, try, and cleanup. State that must cross job boundaries — bucket name, image ref, job ID, endpoint URL — travels as job outputs. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Five Acts of This Drama
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Act 1 — &lt;code&gt;submit&lt;/code&gt;: Everything Happens Here
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;submit&lt;/code&gt; job is where the magic (and most of the YAML) lives. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Writes an &lt;strong&gt;Axolotl config inline&lt;/strong&gt; — the workflow is the single source of truth. No external config file to lose.&lt;/li&gt;
&lt;li&gt;Writes a &lt;strong&gt;&lt;code&gt;train-and-push.sh&lt;/code&gt; script inline&lt;/strong&gt; — yes, a full Bash script, heredoc'd into the workflow. It fine-tunes with Axolotl, packages the adapters into a tar layer, installs &lt;code&gt;crane&lt;/code&gt; (a daemonless container registry client, because the GPU job has no Docker daemon), and pushes &lt;code&gt;vllm/vllm-openai:latest&lt;/code&gt; + the adapter layer to the Nebius Container Registry.&lt;/li&gt;
&lt;li&gt;Provisions a &lt;strong&gt;fresh S3 bucket per run&lt;/strong&gt;, uploads both files, and creates the Nebius Job. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The auth story is pleasantly clean: a &lt;code&gt;setup&lt;/code&gt; action configures a key-based CLI profile, and an &lt;code&gt;auth&lt;/code&gt; action mints a short-lived &lt;code&gt;NEBIUS_IAM_TOKEN&lt;/code&gt; for the SDK path. No long-lived credentials sitting in your repo — just a token that expires before you finish reading this post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Submit fine-tune + image push&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;submit&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/submit-job&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-axolotl-${{ github.run_id }}&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.NEBIUS_JOB_IMAGE || 'docker.io/axolotlai/axolotl:main-20260309-py3.11-cu128-2.9.1' }}&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.platform }}&lt;/span&gt;
    &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.preset }}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;BASE_IMAGE=${{ vars.NEBIUS_VLLM_IMAGE || 'docker.io/vllm/vllm-openai:latest' }}&lt;/span&gt;
      &lt;span class="s"&gt;IMAGE=${{ steps.image.outputs.image }}&lt;/span&gt;
      &lt;span class="s"&gt;REGISTRY=${{ steps.image.outputs.registry }}&lt;/span&gt;
      &lt;span class="s"&gt;REGISTRY_TOKEN=${{ env.NEBIUS_IAM_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;mounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.bucket.outputs.bucket-id }}:/workspace/data:rw&lt;/span&gt;
    &lt;span class="na"&gt;disk-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.disk-size }}&lt;/span&gt;
    &lt;span class="na"&gt;preemptible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-c "bash /workspace/data/train-and-push.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Act 2 — &lt;code&gt;wait&lt;/code&gt;: The Anxious Polling Phase
&lt;/h3&gt;

&lt;p&gt;This job does one thing: stream the GPU job's logs and poll it to a terminal state. It also handles a subtle but expensive problem — if you cancel the GitHub workflow, the Nebius Job keeps running (and keeps billing you) unless you explicitly forward the cancellation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for fine-tune + image push&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wait&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/wait-for-job&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;job-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.submit.outputs.job-id }}&lt;/span&gt;
    &lt;span class="na"&gt;stream-logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;poll-interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cancel job on workflow cancellation&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cancelled() &amp;amp;&amp;amp; needs.submit.outputs.job-id != ''&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/cancel-job&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;job-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.submit.outputs.job-id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;if: cancelled()&lt;/code&gt; guard has saved me from some spicy cloud bills.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act 3 — &lt;code&gt;deploy&lt;/code&gt;: It Lives
&lt;/h3&gt;

&lt;p&gt;Once &lt;code&gt;wait&lt;/code&gt; confirms the job succeeded — meaning the vLLM image with baked-in adapters is in the registry — &lt;code&gt;deploy&lt;/code&gt; creates a Nebius Endpoint and polls it to serving. &lt;code&gt;wait: false&lt;/code&gt; is intentional: it means the endpoint ID is published even if the endpoint never becomes ready, which lets &lt;code&gt;cleanup&lt;/code&gt; tear it down safely regardless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy endpoint&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/deploy-endpoint&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-axolotl-${{ github.run_id }}&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.submit.outputs.image }}&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.endpoint-preset }}&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.endpoint-platform }}&lt;/span&gt;
    &lt;span class="na"&gt;disk-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.endpoint-disk-size }}&lt;/span&gt;
    &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.NEBIUS_PROJECT_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for endpoint&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;endpoint&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/wait-for-endpoint&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy.outputs.endpoint-id }}&lt;/span&gt;
    &lt;span class="na"&gt;poll-interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1800&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Act 4 — &lt;code&gt;try&lt;/code&gt;: Does It Actually Work?
&lt;/h3&gt;

&lt;p&gt;The smoke test is beautifully minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;--retry&lt;/span&gt; 30 &lt;span class="nt"&gt;--retry-delay&lt;/span&gt; 10 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/health"&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/completions"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "demo", "prompt": "The quick brown fox", "max_tokens": 16}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;demo&lt;/code&gt; is the &lt;code&gt;--lora-modules&lt;/code&gt; name that &lt;code&gt;train-and-push.sh&lt;/code&gt; baked into the image entrypoint. You fine-tuned it, you named it, now you serve it. Deeply personal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act 5 — &lt;code&gt;cleanup&lt;/code&gt;: &lt;code&gt;if: always()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The cleanup job has &lt;code&gt;needs&lt;/code&gt; on all four upstream jobs and — critically — &lt;code&gt;if: always()&lt;/code&gt;. This is the move that separates people who have been surprised by a $200 cloud bill from people who haven't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;submit&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;wait&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;try&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete endpoint&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.deploy.outputs.endpoint-id != ''&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/delete-endpoint&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.deploy.outputs.endpoint-id }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete bucket&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.submit.outputs.bucket-id != ''&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./actions/delete-bucket&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.submit.outputs.bucket-name }}&lt;/span&gt;
        &lt;span class="na"&gt;bucket-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.submit.outputs.bucket-id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Endpoint dies. Bucket dies. The pushed image stays in the registry, tagged with the run ID, ready to be redeployed. Responsible MLOps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actions Themselves
&lt;/h2&gt;

&lt;p&gt;The reusable building blocks are small and composable. The key ones: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setup&lt;/code&gt;&lt;/strong&gt; — installs the &lt;code&gt;nebius&lt;/code&gt; CLI, configures a key-based profile, exports &lt;code&gt;NEBIUS_PROJECT_ID&lt;/code&gt; and &lt;code&gt;NEBIUS_SERVICE_ACCOUNT_ID&lt;/code&gt; for downstream steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;auth&lt;/code&gt;&lt;/strong&gt; — mints a short-lived IAM token via OIDC (keyless, recommended) or a service-account key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;submit-job&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;wait-for-job&lt;/code&gt;&lt;/strong&gt; — decoupled create and poll, so you can fan out or gate between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deploy-endpoint&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;wait-for-endpoint&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;delete-endpoint&lt;/code&gt;&lt;/strong&gt; — the full endpoint lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create-bucket&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;upload-object&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;delete-bucket&lt;/code&gt;&lt;/strong&gt; — the storage trio, all using short-lived S3 keys minted on the fly, no stored S3 credentials required. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design philosophy: one action per resource, no hidden orchestration, no magic. If you want a rigid pipeline, the demo is there. If you want to mix and match — say, fine-tune in one workflow and deploy in another — the primitives are waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/piotrjanik/nebius-actions" rel="noopener noreferrer"&gt;github.com/piotrjanik/nebius-actions&lt;/a&gt;. You'll need a Nebius project, a service account with &lt;code&gt;ai.editor&lt;/code&gt; role, and the patience to fill in five repo variables and one secret. The README has the exact &lt;code&gt;nebius iam&lt;/code&gt; commands to set it all up. &lt;/p&gt;

&lt;p&gt;Then click &lt;strong&gt;Run workflow&lt;/strong&gt;, pick your GPU preset, and watch a 500M-parameter model get fine-tuned and deployed while you make coffee. That's the dream. That's the YAML.&lt;/p&gt;

</description>
      <category>nebiusserverlesschallenge</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
