Devops8 min read

Replace Github Actions With One Command

My GitHub Actions deploy workflow was 87 lines of YAML. It broke constantly, took 12 minutes, and I was the only one who understood it. Here's what I replaced it with.

P

Platform Super Admin

April 4, 2026

Replace Github Actions With One  Command

My GitHub Actions deploy workflow was 87 lines of YAML.

It had grown over 18 months from a clean 20-line file into something I was genuinely afraid to touch. It broke whenever a dependency updated. It had three hardcoded ARNs from an AWS account I was no longer using. It had a comment that said # TODO: fix this that had been there for 11 months.

Last month I deleted all 87 lines and replaced them with one command.

Here's exactly how I did it — and what I learned along the way.


The YAML graveyard

This was my deploy workflow. See if any of this feels familiar:

yaml
1name: Deploy to Production
2
3on:
4  push:
5    branches: [main]
6
7jobs:
8  deploy:
9    runs-on: ubuntu-latest
10    steps:
11      - name: Checkout
12        uses: actions/checkout@v3
13
14      - name: Set up Node.js
15        uses: actions/setup-node@v3
16        with:
17          node-version: '20'
18          cache: 'npm'
19
20      - name: Install dependencies
21        run: npm ci
22
23      - name: Run tests
24        run: npm test
25
26      - name: Configure AWS credentials
27        uses: aws-actions/configure-aws-credentials@v2
28        with:
29          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
30          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
31          aws-region: us-east-1
32
33      - name: Login to Amazon ECR
34        id: login-ecr
35        uses: aws-actions/amazon-ecr-login@v1
36
37      - name: Build Docker image
38        env:
39          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
40          IMAGE_TAG: ${{ github.sha }}
41        run: |
42          docker build -t $ECR_REGISTRY/my-app:$IMAGE_TAG .
43          docker push $ECR_REGISTRY/my-app:$IMAGE_TAG
44          echo "IMAGE=$ECR_REGISTRY/my-app:$IMAGE_TAG" >> $GITHUB_ENV
45
46      - name: Download task definition
47        run: |
48          aws ecs describe-task-definition --task-definition my-app \
49            --query taskDefinition > task-definition.json
50
51      - name: Update ECS task definition
52        id: task-def
53        uses: aws-actions/amazon-ecs-render-task-definition@v1
54        with:
55          task-definition: task-definition.json
56          container-name: my-app
57          image: ${{ env.IMAGE }}
58
59      - name: Deploy to ECS
60        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
61        with:
62          task-definition: ${{ steps.task-def.outputs.task-definition }}
63          service: my-app-service
64          cluster: my-app-cluster
65          wait-for-service-stability: true
66
67      - name: Notify on failure
68        if: failure()
69        run: |
70          curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
71            -H 'Content-type: application/json' \
72            --data '{"text":"Deploy failed! Check Actions."}'

I wrote this. I'm not proud of it.

The real problem wasn't the YAML itself. The problem was everything hidden underneath the YAML:

  • An ECR repository I had to provision manually
  • An ECS cluster, service, and task definition I had to set up in the console
  • IAM roles with the exact right permissions (I guessed wrong twice)
  • A Dockerfile I maintained separately
  • AWS credentials rotated manually every 90 days

The pipeline was the visible part. The invisible part was 3 days of setup I did 18 months ago that I could no longer remember well enough to recreate.

When a new teammate joined and asked "how does deploy work?" — I sent them the workflow file and said "it's complicated."

That's not an answer. That's a warning sign.


The breaking point

In February, I switched from Node 18 to Node 20. The Docker build broke because my base image was pinned to node:18-alpine in three different places — the Dockerfile, the Actions workflow, and a .nvmrc file I had forgotten existed.

The fix took 45 minutes. The error message was not helpful. I fixed it by diffing my Dockerfile against a Stack Overflow answer from 2023.

Two weeks later, AWS deprecated the amazon-ecs-render-task-definition@v1 action. The pipeline broke silently — it ran, reported success, but the new image never actually deployed. I found out because a user filed a bug for something I had definitely already fixed.

That was the moment I decided: the pipeline is not worth maintaining.


What I tried first

I looked at Render and Railway. Both are good products. Neither deploys into my own AWS account — they provision their own infrastructure. My company has a compliance requirement that customer data stays in a customer-owned AWS environment. So those were out.

I looked at AWS CodePipeline. I wanted to solve complexity, not add more of it.

Then a colleague mentioned NEXUS AI. He described it as "a CLI that handles all the ECS stuff so you don't have to." I was skeptical. That's what everyone says.


The initial deploy

I installed the CLI:

bash
npm install -g @nexusai/cli
nexus login

Then I pointed it at my repo:

bash
nexus deploy source \
  --repo https://github.com/myorg/my-app \
  --name my-app \
  --provider aws_ecs_fargate

I expected this to fail immediately. My expectations for new DevOps tools are calibrated by years of experience.

It didn't fail. Four and a half minutes later I got back a URL. The app was running. The same app. In my AWS account.

I checked the AWS console out of habit. There was an ECS cluster. A task definition. A service. An ECR repository with the image in it. NEXUS AI had provisioned all of it.

I had not written a Dockerfile. I had not configured any IAM roles. I had not touched the AWS console.

I sat with that for a moment.


What actually happens under the hood

Here's what nexus deploy source does, in order:

  1. Reads your repo — detects the runtime from package.json, requirements.txt, go.mod, etc.
  2. Builds the container on NEXUS AI's build infrastructure — not your machine, not a GitHub runner
  3. Pushes the image to an ECR repository it provisions in your account
  4. Creates (or updates) the ECS infrastructure — cluster, task definition, service, load balancer
  5. Issues a TLS certificate via ACM and wires it to the load balancer
  6. Waits for health checks to pass before returning the live URL

Steps 3–5 are the 3 days of manual work I did 18 months ago. They now run in parallel and take about 3 minutes.


The new GitHub Actions workflow

Here's my deploy workflow today:

yaml
1name: Deploy to Production
2
3on:
4  push:
5    branches: [main]
6
7jobs:
8  test:
9    runs-on: ubuntu-latest
10    steps:
11      - uses: actions/checkout@v4
12      - uses: actions/setup-node@v4
13        with:
14          node-version: '20'
15          cache: 'npm'
16      - run: npm ci
17      - run: npm test
18
19  deploy:
20    needs: test
21    runs-on: ubuntu-latest
22    steps:
23      - name: Deploy
24        run: nexus deploy redeploy --deployment-id ${{ secrets.NEXUSAI_DEPLOYMENT_ID }}
25        env:
26          NEXUSAI_TOKEN: ${{ secrets.NEXUSAI_TOKEN }}

24 lines, including name: fields and blank lines.

The deploy job has one step. It calls nexus deploy redeploy, which tells NEXUS AI to rebuild from the latest commit and roll it out with a rolling update. No Docker commands. No AWS credentials. No ECR. No ECS task definition wrangling.

I kept the test job. NEXUS AI doesn't replace your test suite — it replaces everything after tests pass.


Secrets and environment variables

Before, I had secrets in three places: GitHub Actions secrets (for the pipeline), AWS Secrets Manager (for the app), and a .env.example file that was always slightly out of date.

Now:

bash
nexus secret set \
  DATABASE_URL=postgres://user:pass@host/db \
  STRIPE_SECRET_KEY=sk_live_... \
  NODE_ENV=production

These are encrypted at rest and injected as environment variables when the container starts. The pipeline only needs NEXUSAI_TOKEN — one secret instead of seven.

After updating secrets, one command applies them:

bash
nexus deploy redeploy --deployment-id <your-deployment-id>

Rollback

Old workflow rollback: figure out the previous image SHA, manually update the ECS task definition, trigger a new deployment, hope the old image hasn't been cleaned up by the ECR lifecycle policy.

New rollback:

bash
nexus deploy rollback --deployment-id <your-deployment-id>

Reverts to the previous container image. Health checks run. Done. I've used this twice. Both times took under 90 seconds.


Redeployment speed

First deploy: ~4.5 minutes (infrastructure provisioning included).

Subsequent deploys: 60–90 seconds. Infrastructure is already provisioned, so it's just build → push → rolling update.

My old pipeline took 10–12 minutes. Most of that was the Docker build running on GitHub's shared runners plus the ECS service stability wait.


What I lost

Every tool has trade-offs. These are the real ones:

Less visibility into the build environment. With a Dockerfile I wrote, I knew exactly what was in the image. With source-based deployment, NEXUS AI generates the image. You can inspect it — nexus deploy logs gives the full build output — but you're not authoring the Dockerfile. For most apps this is fine. If you have specific system dependencies (custom C extensions, obscure shared libraries), test carefully.

The first deploy takes time. Infrastructure provisioning isn't instant. If you need sub-30-second cold deploys for some reason, this isn't that. But once infrastructure exists, redeployments are fast.

You're adding a dependency. NEXUS AI is now in your deploy path. Worth knowing.


The numbers

Old workflowNew workflow
Lines of YAML8724
Pipeline runtime10–12 min60–90 sec
AWS console setup~3 days (one-time)0
Secrets locations31
Rollback steps~6 manual steps1 command
Last random breakageNovemberHasn't happened

How to try it

bash
1# Install
2npm install -g @nexusai/cli
3
4# Authenticate
5nexus login
6
7# First deploy — detects Node/Python/Go automatically, no Dockerfile needed
8nexus deploy source \
9  --repo https://github.com/your/repo \
10  --name my-app \
11  --provider aws_ecs_fargate   # or gcp_cloud_run, azure_container_apps
12
13# Check status
14nexus deploy status --deployment-id <id>
15
16# Set environment variables
17nexus secret set KEY=value KEY2=value2
18
19# Redeploy (use this in CI)
20nexus deploy redeploy --deployment-id <id>
21
22# Rollback
23nexus deploy rollback --deployment-id <id>

For CI/CD: add NEXUSAI_TOKEN and NEXUSAI_DEPLOYMENT_ID as secrets in your GitHub repo settings, then replace your deploy steps with the one-liner from the workflow above.


Final thought

The 87-line YAML file wasn't the real cost. The real cost was the cognitive overhead of owning it — the 45-minute debugging session when Node versions drifted, the silent failure when an Action was deprecated, the "it's complicated" I sent to a new teammate.

I don't miss any of that.

If you're maintaining a pipeline like the one I had, it's worth spending 20 minutes to find out how much of it you can delete.


Building something and want to compare notes? Drop it in the comments.

Comments

Log in to leave a comment.

Get started today

Ship faster. Scale effortlessly.

Deploy your first app in minutes. No DevOps team required. Full AI observability from day one.

No credit card required · Free tier available