AI generation API GitHub Actions: make asset generation an explicit job
·
Asset generation in CI should be an intentional workflow, because rerunning a submission step creates new work. Begin with a manually dispatched image workflow rather than attaching generation to every pull request. This example stores the accepted job and the final observation as artifacts, allowing a later operator to distinguish a rendering problem from an interrupted CI runner.
1. Configure a repository secret
Create a GitHub Actions secret named GENACE_API_KEY using a key intended for this workflow. Limit who can dispatch the workflow through your repository settings and access policy. Do not expose the key through workflow inputs, echo it, or enable shell tracing while it is present.
Create .github/workflows/generate-image.yml. The sample requires the standard Ubuntu runner tools curl and jq. It uses an explicit run group to prevent two executions of this workflow from submitting simultaneously in the same repository.
name: Generate image asset
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: generate-image-asset
cancel-in-progress: false
jobs:
generate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Submit and observe
env:
GENACE_API_KEY: ${{ secrets.GENACE_API_KEY }}
shell: bash
run: |
set -euo pipefail
: "${GENACE_API_KEY:?GENACE_API_KEY is required}"
mkdir -p result
curl --silent --show-error --fail-with-body --max-time 45 \
https://genace.ai/api/v1/images/generations \
-H "Authorization: Bearer $GENACE_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"model":"nano-banana","prompt":"A watercolor mountain landscape","n":1}' \
-o result/accepted.json
JOB_ID=$(jq -er '.job_id' result/accepted.json)
for ATTEMPT in $(seq 1 20); do
curl --silent --show-error --fail-with-body --max-time 45 \
"https://genace.ai/api/v1/jobs/$JOB_ID?wait=30" \
-H "Authorization: Bearer $GENACE_API_KEY" \
-o result/status.json
STATE=$(jq -er '.state' result/status.json)
case "$STATE" in
succeeded) exit 0 ;;
failed|canceled) exit 1 ;;
esac
done
exit 1
- name: Preserve job evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: generation-job-${{ github.run_id }}-${{ github.run_attempt }}
path: result/
2. Separate a rerun from a resume
The concurrency group limits active executions. With this default queue policy,
only one run can be pending; a newer waiting run replaces the existing pending
run even though cancel-in-progress: false keeps the running one. Do not treat
it as a durable queue of every dispatch or as API operation deduplication.
Rerunning the workflow submits again because the first step contains a POST.
To resume an interrupted job, create a separate observation workflow accepting a
known job ID and containing only the GET loop.
An unset GitHub secret expression becomes an empty string. set -u alone does
not reject that value. The explicit shell guard rejects both unset and empty
values before curl runs; it checks presence, not credential validity. Use the
run ID and attempt in the artifact name to distinguish recovery evidence from
different reruns without overwriting an earlier artifact. This naming choice
does not make a rerun safe to submit or guarantee artifact retention.
An artifact can contain prompts, output URLs, and credit information. Apply the repository’s retention and access policy accordingly. Artifact upload in a final step helps with ordinary step failures, but no workflow file can guarantee an artifact after every infrastructure interruption. For stronger recovery, persist accepted job IDs in your own durable system immediately after submission.
The submission file is a raw response, despite its accepted.json filename.
--fail-with-body can leave an error body there while curl exits unsuccessfully.
A known submission 502 may contain error.job_id: inspect that same job rather
than rerunning the POST. Distinguish UPSTREAM_SUBMISSION_FAILED from
UPSTREAM_SUBMISSION_UNCERTAIN; rejection does not prove a refund settled,
and unknown acceptance does not authorize automatic refunds or cancellation
that bypasses reconciliation. Transport failure may leave no reliable body or ID.
3. Treat completion as a state decision
The job endpoint returns HTTP 200 for a successfully retrieved job even when that generation failed. The loop therefore reads state, exits successfully only on succeeded, and fails on failed or canceled. A timeout also produces a failed CI execution, but it does not mark the remote job failed or cancel it.
Before adding publishing steps, validate that succeeded responses contain the expected image outputs. Keep image download and website publication in later steps with their own error handling. A failed download should retry the existing output URL rather than repeat generation.
Common mistakes and verification
Do not run this secret-bearing workflow against arbitrary unreviewed repository code. Keep the generation body fixed initially, and review any future prompt inputs before allowing shell interpolation. This example uses a literal JSON body so user text cannot become shell syntax.
Verify the workflow YAML parses, the secret is present without printing its value, failed fixtures exit unsuccessfully, and accepted responses remain available for diagnosis. Confirm your team understands that pressing rerun can incur another generation.
Use curl image diagnostics when testing the HTTP steps, and environment variable guidance when coordinating local and hosted credentials.
Continue with code and integration
Models covered on this page
Where these facts come from
- Editorial keyword hypothesis; search volume not measured
- codebase: src/app/api/v1/images/generations/route.ts
- codebase: src/ai/jobs/http.ts
- https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets
- https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- https://docs.github.com/en/actions/reference/workflows-and-actions/variables
- https://github.com/actions/upload-artifact/tree/v4