agentsclimarketplace

Cloud build

Skill JordanChoo/acfs-agent-skills/cloud-build

Agent Flywheel Coding Skills

Install
npx -y skills add JordanChoo/acfs-agent-skills --skill cloud-build

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Discipline for Google Cloud Build CI/CD. Use when the repo contains `cloudbuild.yaml` / `cloudbuild.*.yaml` / `.gcloudignore`, when running `gcloud builds submit` or `gcloud builds triggers ...`, when authoring or debugging a Cloud Build pipeline, when wiring CB to Artifact Registry, Cloud Run, Cloud Run Jobs, GKE, or Firebase deploys, or when the user mentions cloudbuild, build triggers, build steps, substitutions, or build pools. Encodes the schema, the trigger surface, the user-SA + logging trap, secret manager wiring, DAG step ordering (waitFor), substitutions ($$ escaping + dynamic), and the scry-style gold-standard multi-stage pipeline.

SKILL.md

24.9 KB, as published. Nobody here has run it

cloud-build

Discipline for working with Google Cloud Build CI/CD. The goal is to prevent the recurring traps: misordered waitFor DAGs, the user-service-account logging trap, secret references that silently expand to empty, triggers that don't fire because of branch/region mismatch, and pipelines that exceed the default 60-min timeout.

Triggers

Load this skill when ANY of:

  • Files present: cloudbuild.yaml, cloudbuild.*.yaml, cloudbuild.*.json, .gcloudignore, a deploy/cloudbuild* directory
  • User mentions: cloud build, cloudbuild, gcloud builds, build trigger, build step, build pool, substitutions, secretEnv, BUILD_ID, SHORT_SHA, Cloud Run deploy from CB, Firebase deploy via CB, Artifact Registry push from CB
  • A gcloud builds command is about to be run or has just failed

Pre-Flight: discover the project's existing pipeline

Before writing new YAML, read what already exists. Repos in this environment usually have multiple cloudbuild files for distinct flows:

cloudbuild.yaml                    # main CI/CD on push to main
cloudbuild.validate.yaml           # PR / branch validation (no deploy)
cloudbuild.function-deploy.yaml    # parameterized per-function deploy
deploy/cloudbuild.yaml             # release pipeline (container build → push → Cloud Run)

A cloudbuild.*.yaml is the source of truth — don't infer the pipeline from CI scripts. Read it.

If the repo ships its own scripts (scripts/run-ci-e2e-with-emulators.sh, scripts/check-firebase-deploy-prereqs.sh, scripts/provision-queues.sh), the cloudbuild file will call them via entrypoint: bash. Don't reimplement what the scripts already encode.

Decision Tree

What is the task?
│
├─ "Run a build now, locally"
│   → gcloud builds submit --config=cloudbuild.yaml --region=<region> . (§Recipes #1)
│
├─ "Build & push a Docker image, nothing else"
│   → gcloud builds submit --tag=LOCATION-docker.pkg.dev/PROJECT/REPO/IMG . (§Recipes #2)
│
├─ "Author a multi-step pipeline"
│   → §Recipes #3 (DAG with id/waitFor) + §Schema cheat sheet
│
├─ "Wire secrets into a step"
│   → §Recipes #4 (availableSecrets + secretEnv)
│
├─ "Deploy to Cloud Run / Functions / Firebase from CB"
│   → §Recipes #5 (impersonated cloud-sdk step)
│
├─ "Create a trigger on push / PR / tag"
│   → §Recipes #6 (gcloud builds triggers create github)
│
├─ "Build failed — what now?"
│   → §Troubleshooting (parse the error class first)
│
└─ "Move from default to user-specified service account"
    → §Pitfall P1 BEFORE editing anything else

Schema cheat sheet (the parts that matter)

Top-level keys, in order of how often you touch them:

steps: [ ... ]               # required; up to 300 steps
substitutions: { _KEY: val } # user-defined; must start with _, [A-Z0-9_]+
availableSecrets:            # preferred; integrates Secret Manager
  secretManager:
    - versionName: projects/PROJECT_ID/secrets/NAME/versions/latest
      env: VAR_NAME
options:
  logging: CLOUD_LOGGING_ONLY        # see Pitfall P1 — required with user SA
  machineType: E2_HIGHCPU_8          # default E2_STANDARD_2 (2 vCPU)
  diskSizeGb: 100                    # default 100, max 4000
  dynamicSubstitutions: true         # enable ${_VAR} expansion in substitutions
  substitutionOption: ALLOW_LOOSE    # tolerate undefined subs (auto for triggers)
  pool: { name: projects/P/locations/L/workerPools/POOL }   # private pool
timeout: '3600s'             # default 60m; raise BEFORE the build hits 1h
queueTtl: '3600s'            # default 1h queued wait
tags: [ ci, prod-deploy ]    # for filtering in `gcloud builds list`
images: [ gcr.io/P/img ]     # images CB should push on success
artifacts: { objects: { location: gs://..., paths: [...] } }
logsBucket: gs://my-logs     # ONLY if not using CLOUD_LOGGING_ONLY
serviceAccount: projects/P/serviceAccounts/[email protected]

Per-step fields:

- name: node:22                  # required; builder image
  entrypoint: bash               # override; common for inline shell
  args: ['-c', '|', ...]         # OR `script:` (but not both)
  dir: functions                 # cwd within /workspace
  id: build-functions            # name this step (required for waitFor)
  waitFor: ['install-functions'] # DAG: only after these complete; ['-'] = run at start
  env: ['FOO=bar']               # plaintext env
  secretEnv: ['API_KEY']         # secret env (must appear in availableSecrets)
  timeout: '1200s'               # step-level timeout
  allowFailure: true             # don't fail the build if this step fails
  allowExitCodes: [0, 2]         # treat these as success
  volumes: [{ name: cache, path: /workspace/.cache }]

waitFor rules:

  • Default (omitted): waits for all earlier steps to finish.
  • waitFor: ['-']: runs immediately, in parallel with the first step (no deps).
  • waitFor: [id1, id2]: runs after those step IDs complete successfully.
  • Use id: everywhere if you want explicit DAG control — implicit ordering is footgun-prone.

Default substitutions

Always available:

VariableContains
$PROJECT_IDProject ID
$BUILD_IDThis build's UUID
$PROJECT_NUMBERNumeric project ID
$LOCATIONBuild region

Trigger-only (empty in gcloud builds submit runs without a connected repo):

VariableContains
$COMMIT_SHA / $REVISION_IDFull git SHA
$SHORT_SHAFirst 7 chars of $COMMIT_SHA
$BRANCH_NAME / $TAG_NAME / $REF_NAMEGit ref
$REPO_NAME / $REPO_FULL_NAMERepo identity
$TRIGGER_NAMEName of invoking trigger
$_HEAD_BRANCH / $_BASE_BRANCH / $_HEAD_REPO_URL / $_PR_NUMBERPR triggers only

User subs: must match _[A-Z0-9_]+, max 200 per build. Override at submit time with --substitutions=_KEY=val,_OTHER=val2. $PROJECT_ID and $BUILD_ID cannot be overridden.

Dollar-sign rules (the #1 source of "why is my secret empty" confusion):

  • $_FOO → substitution value (CB substitutes BEFORE the step runs)
  • $$_FOO → literal $_FOO (escapes CB substitution; shell still sees $_FOO)
  • ${_FOO} → same as $_FOO but lets you concat: ${_FOO}BAR
  • When using secretEnv, reference the secret as $$VAR in args: strings so CB doesn't try to substitute it at config-parse time. In entrypoint: bash scripts the shell expansion takes over and $VAR works normally inside the script body.

Real Command Surface

Submit & inspect

CommandUse it for
gcloud builds submit --config=cloudbuild.yaml --region=<r> .Run the YAML against the current dir
gcloud builds submit --tag=LOC-docker.pkg.dev/P/R/IMG .Dockerfile-only build & push
gcloud builds submit --pack image=LOC.../IMG .Buildpacks build (no Dockerfile)
gcloud builds submit --no-source --config=cloudbuild.yamlSourceless build (no upload)
gcloud builds submit ... --substitutions=_KEY=val,_OTHER=val2Override user subs
gcloud builds submit ... --asyncDon't block on completion
gcloud builds submit ... --service-account=projects/P/serviceAccounts/SA --default-buckets-behavior=REGIONAL_USER_OWNED_BUCKETUse a user SA
gcloud builds list --region=<r> --ongoing --limit=10What's running right now
gcloud builds describe <BUILD_ID> --region=<r>Full build record
gcloud builds log <BUILD_ID> --region=<r>Print logs for a finished build
gcloud builds log <BUILD_ID> --region=<r> --streamTail logs of a running build
gcloud builds cancel <BUILD_ID> --region=<r>Stop a running build

Always set --region if the trigger or build is regional (default is global). Mismatched region is a top cause of "I don't see my build."

Triggers

CommandUse it for
gcloud builds triggers create github --name=N --repo-owner=O --repo-name=R --branch-pattern='^main$' --build-config=cloudbuild.yaml --region=<r>Create push trigger on main
... --tag-pattern='^v\d+\.\d+\.\d+$'Create tag-release trigger
... --pull-request-pattern='^main$'Create PR trigger (GitHub App)
... --service-account=projects/P/serviceAccounts/SABind a user SA (overrides config)
... --require-approvalManual approval gate before execution
... --included-files='src/**' --ignored-files='docs/**'Path filters
... --substitutions=_ENV=prodTrigger-level substitution overrides
gcloud builds triggers list --region=<r>List triggers
gcloud builds triggers describe <T> --region=<r>Full trigger record
gcloud builds triggers run <T> --branch=main --region=<r>Manually fire
gcloud builds triggers export <T> --destination=trigger.yaml --region=<r>Round-trip to YAML
gcloud builds triggers import --source=trigger.yaml --region=<r>Re-apply edited YAML
gcloud builds triggers delete <T> --region=<r>Remove

Trigger gotcha: when a trigger has its own --service-account, the serviceAccount: in the build config is ignored. Edit the trigger, not the YAML.

Pools

CommandUse it for
gcloud builds worker-pools create POOL --region=<r> --peered-network=projects/P/global/networks/VPCCreate private pool (VPC-attached)
gcloud builds worker-pools describe POOL --region=<r>Pool details

Recipes

1. Run an existing pipeline locally

gcloud builds submit --config=cloudbuild.yaml --region=us-central1 .
# Override a sub:
gcloud builds submit --config=cloudbuild.yaml --region=us-central1 \
  --substitutions=_ENV=staging,_IMAGE_TAG=$(git rev-parse --short HEAD) .

Files are uploaded per .gcloudignore (auto-derived from .gitignore if missing). Add .git/ and large fixtures explicitly if your tarball is huge.

2. Minimal Docker build & push (no YAML needed)

gcloud builds submit \
  --tag=us-central1-docker.pkg.dev/$PROJECT/my-repo/app:$(git rev-parse --short HEAD) \
  --region=us-central1 .

Cloud Build runs docker build against the local Dockerfile, pushes to Artifact Registry, and tags with the value you supplied. Use this for the simplest case; switch to a cloudbuild.yaml the moment you need a second step.

3. Multi-step pipeline with DAG ordering

steps:
  - name: 'node:22'
    entrypoint: 'npm'
    args: ['ci']
    id: 'install'

  # These three run in parallel after install
  - name: 'node:22'
    entrypoint: 'npm'
    args: ['run', 'lint']
    id: 'lint'
    waitFor: ['install']

  - name: 'node:22'
    entrypoint: 'npm'
    args: ['run', 'test']
    id: 'test'
    waitFor: ['install']

  - name: 'node:22'
    entrypoint: 'npm'
    args: ['run', 'build']
    id: 'build'
    waitFor: ['install']

  # Fan-in: deploy only after all three pass
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -euo pipefail
        gcloud run deploy app --image=us-central1-docker.pkg.dev/$PROJECT_ID/repo/app:$SHORT_SHA --region=us-central1
    id: 'deploy'
    waitFor: ['lint', 'test', 'build']

options:
  logging: CLOUD_LOGGING_ONLY
timeout: '1800s'

Discipline: every step gets id: and explicit waitFor:. Implicit ordering bites the moment someone reorders steps in a PR.

4. Secret Manager wiring

steps:
  - name: 'node:22'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -euo pipefail
        # In a bash script the shell expands env normally:
        echo "Building with API key: ${API_KEY:0:4}..." # don't actually log keys
        npm run build
    id: 'build'
    secretEnv: ['API_KEY']

availableSecrets:
  secretManager:
    - versionName: projects/$PROJECT_ID/secrets/MY_API_KEY/versions/latest
      env: 'API_KEY'

Required IAM: the build service account (default or user-specified) needs roles/secretmanager.secretAccessor on the secret (or project-wide). Without it, the secret silently resolves to empty and your step gets a blank string.

When passing a secret to a tool via args: without entrypoint: bash, escape with $$:

- name: 'gcr.io/cloud-builders/docker'
  args: ['login', '-u', 'user', '-p', '$$DOCKER_PW']
  secretEnv: ['DOCKER_PW']

5. Deploy to Cloud Run (or Functions/Firebase) from a CB step

- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
  entrypoint: 'bash'
  args:
    - '-c'
    - |
      set -euo pipefail
      gcloud run deploy my-service \
        --image="us-central1-docker.pkg.dev/${PROJECT_ID}/my-repo/app:${SHORT_SHA}" \
        --region=us-central1 \
        --service-account="runtime-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --quiet
  id: 'deploy-cloud-run'
  env:
    # Use this to delegate to a deploy SA *from within the step*:
    - 'CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT=deployer@${PROJECT_ID}.iam.gserviceaccount.com'

The build SA needs roles/iam.serviceAccountTokenCreator on the impersonated SA, and the impersonated SA needs the deploy roles (roles/run.admin + roles/iam.serviceAccountUser on the runtime SA).

For Firebase deploys, use a node:22 step that runs ./node_modules/.bin/firebase deploy --project=PROJECT --only=<targets> --non-interactive --force. The CB SA needs Firebase Admin and the same Service Account User role on the Functions runtime SA.

6. Create a push trigger on main

gcloud builds triggers create github \
  --name=ci-main \
  --region=us-central1 \
  --repo-owner=YourOrg --repo-name=YourRepo \
  --branch-pattern='^main$' \
  --build-config=cloudbuild.yaml \
  --service-account=projects/$PROJECT_ID/serviceAccounts/ci-sa@$PROJECT_ID.iam.gserviceaccount.com \
  --included-files='src/**,cloudbuild.yaml' \
  --ignored-files='docs/**,**/*.md'

Then a PR-only validation trigger pointing to a thinner config:

gcloud builds triggers create github \
  --name=ci-pr-validate \
  --region=us-central1 \
  --repo-owner=YourOrg --repo-name=YourRepo \
  --pull-request-pattern='^main$' \
  --build-config=cloudbuild.validate.yaml \
  --comment-control=COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY

Edit triggers via export/import, not click-ops:

gcloud builds triggers export ci-main --destination=triggers/ci-main.yaml --region=us-central1
# edit triggers/ci-main.yaml
gcloud builds triggers import --source=triggers/ci-main.yaml --region=us-central1

7. Manual approval gate

Add --require-approval to the trigger. Approvers need roles/cloudbuild.builds.approver. The build sits in PENDING until an approver acts via console, gcloud builds approve <BUILD_ID>, or API.

8. Parameterized config (one YAML, many invocations)

substitutions:
  _PROJECT_ID: 'my-project'
  _FUNCTION: ''   # required; caller supplies

steps:
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        set -euo pipefail
        if [[ -z "${_FUNCTION}" ]]; then
          echo "_FUNCTION substitution is required" >&2
          exit 1
        fi
        gcloud functions deploy "${_FUNCTION}" --project="${_PROJECT_ID}" ...

Caller: gcloud builds submit --config=cloudbuild.function-deploy.yaml --substitutions=_FUNCTION=myFunc . --no-source (use --no-source when the step doesn't need the repo).


Anti-Patterns

A1. Using a user-specified service account without setting options.logging

Symptom: Build dies immediately with you must specify either logsBucket or set options.logging to CLOUD_LOGGING_ONLY. Cause: Default logging requires write access to the default logs bucket, which only the legacy CB SA has. Fix: Add options: { logging: CLOUD_LOGGING_ONLY } (or specify your own logsBucket: gs://...). Grant roles/logging.logWriter to the user SA. See §Pitfall P1.

A2. Reordering steps without checking waitFor

Symptom: A step suddenly fails because a dependency it implicitly relied on isn't done yet. Fix: Always give every step an id: and an explicit waitFor:. If you intend "no dependency, run at start," use waitFor: ['-']. Treat implicit ordering as a code smell.

A3. Referencing $COMMIT_SHA in a manually-submitted build

Symptom: Empty string substituted; image gets tagged : or :latest accidentally. Cause: $COMMIT_SHA is only set for trigger-invoked builds. Manual gcloud builds submit leaves it empty. Fix: Pass it explicitly: --substitutions=COMMIT_SHA=$(git rev-parse HEAD),SHORT_SHA=$(git rev-parse --short HEAD). Or use substitutionOption: ALLOW_LOOSE and a sane default sub.

A4. Forgetting $$ when passing secrets through args:

Symptom: Secret value appears in build logs (logged before being injected) OR resolves to empty. Fix: In raw args: strings, use $$SECRET_NAME so CB doesn't try to substitute it as a user variable. Inside entrypoint: bash scripts, the secret is in the runtime env — use normal $SECRET_NAME.

A5. Timeouts: default is 60 minutes

Symptom: Build dies at exactly 1h with TIMEOUT. Fix: Set timeout: '3600s' (1h, explicit), '7200s' (2h), or whatever you need. The hard ceiling is 24h. Don't discover this in production — set it from day one for any pipeline that includes container builds or e2e tests.

A6. Building on the default machine and wondering why it's slow

Symptom: npm ci or cargo build takes 15+ min. Fix: options.machineType: E2_HIGHCPU_8 (or E2_HIGHCPU_32 for heavy builds). Costs more per minute but usually finishes faster, and CB bills per minute on the machine size you chose.

A7. Hand-clicking triggers in the console

Symptom: Trigger state is undocumented; nobody knows why it fires on develop instead of main; "what changed?" has no answer. Fix: gcloud builds triggers export to YAML, commit it under triggers/, edit and import. Triggers belong in version control like everything else.

A8. cloudbuild.yaml for "build & push image" when --tag would do

Symptom: 50-line YAML for what is literally docker build && docker push. Fix: Use gcloud builds submit --tag=... until you actually need a second step. Don't add YAML preemptively.

A9. Hard-coding PROJECT_ID instead of using $PROJECT_ID

Symptom: YAML can't be promoted between dev/staging/prod projects without editing. Fix: Always use the default substitution $PROJECT_ID. For other project-specific knobs, use user subs (_REGION, _ARTIFACT_REPO) and pass per-environment via trigger substitutions.

A10. Not understanding dynamicSubstitutions

Symptom: _IMAGE_NAME: '${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/...' produces the literal string, not the expanded path. Fix: Set options.dynamicSubstitutions: true. (It's true by default for trigger-invoked builds, false by default for gcloud builds submit.)


Pitfall P1: User-specified service account (detailed)

This is the single biggest trap. Default SA → custom SA migration usually fails on first build.

Required roles on the new build SA:

  • roles/cloudbuild.builds.builder — execute builds
  • roles/logging.logWriter — required by CLOUD_LOGGING_ONLY
  • roles/storage.objectAdmin — on the staging bucket (for source uploads)
  • roles/artifactregistry.writer — to push images
  • roles/secretmanager.secretAccessor — per secret (or project-wide) for availableSecrets
  • roles/iam.serviceAccountTokenCreator — on any SAs you impersonate in steps

Required in the YAML:

options:
  logging: CLOUD_LOGGING_ONLY    # OR set `logsBucket: gs://...`
serviceAccount: projects/${PROJECT_ID}/serviceAccounts/build-sa@${PROJECT_ID}.iam.gserviceaccount.com

If you specify the SA on a trigger instead, the SA in the YAML is ignored.

Cross-project SAs: org policy iam.disableCrossProjectServiceAccountUsage must be off, and the Cloud Build service agent needs roles/iam.serviceAccountTokenCreator on the foreign SA. The console doesn't support this — use gcloud.


Troubleshooting

SymptomDiagnoseFix
you must specify either logsBucket or set options.loggingUsing a user SA without log configAdd options.logging: CLOUD_LOGGING_ONLY; grant roles/logging.logWriter
PERMISSION_DENIED on a gcloud call inside a stepBuild SA missing the roleIdentify which SA (build SA, not your user) and grant the role
Permission denied while pushing to Artifact RegistryBuild SA missing roles/artifactregistry.writerGrant it on the AR repo
Token exchange failed for SA XTrying to impersonate without roles/iam.serviceAccountTokenCreatorGrant token-creator on the target SA to the build SA
Secret resolves to empty stringSecret accessor role missing OR variable referenced before injectionVerify roles/secretmanager.secretAccessor; use $$VAR in args
Step hangs foreverDefault 60-min build timeout OR step lacks its own timeoutSet timeout: at build + per-step
Build trigger could not be triggeredBranch/tag pattern doesn't match the refTest pattern locally: echo refs/heads/main | grep -E '^refs/heads/main$'
Trigger never fires on pushWrong region OR wrong --included-files maskgcloud builds triggers describe T --region=<r>; check filter:, includedFiles:
Build runs but nothing in the UIRegion mismatch — UI defaults to global, your build is regionalgcloud builds list --region=us-central1 --limit=20
OOM in a stepDefault 4GB on E2_STANDARD_2Bump options.machineType
Forbidden by org policy: iam.allowedPolicyMemberDomainsCross-org SA usage blockedTalk to org admin; consider keeping SAs in-project
Logs streaming hangs in --streamBuild is queued behind concurrent quotagcloud builds list --ongoing --region=<r>; raise concurrent-build quota
Step failed: exit code 137OOM killed the containerBump machine type or refactor the step

First-line debugging recipe:

# 1. Find the build
gcloud builds list --region=us-central1 --limit=5 --filter='status!=SUCCESS'

# 2. Read its full record
gcloud builds describe <BUILD_ID> --region=us-central1

# 3. Pull logs (don't go to the console first — log output is faster from CLI)
gcloud builds log <BUILD_ID> --region=us-central1 | tail -200

House Style (observed in this user's repos)

These patterns recur across scry/cloudbuild.yaml, cloudbuild.validate.yaml, and cloudbuild.function-deploy.yaml. When in doubt, match this style:

  1. Every step has id: and explicit waitFor:. No implicit DAG.
  2. Inline bash steps start with set -euo pipefail. Always.
  3. node:22 is the default builder image for npm/Node work; gcr.io/google.com/cloudsdktool/cloud-sdk:slim is the default for steps that call gcloud / gsutil / bq.
  4. options.logging: CLOUD_LOGGING_ONLY is set in every file.
  5. Pre-deploy gates use CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT to delegate to a deploy SA — the build SA holds roles/iam.serviceAccountTokenCreator on the deploy SA.
  6. Long pipelines use timeout: '3600s'; validation-only pipelines use '1800s'.
  7. Secrets are wired via availableSecrets.secretManager with versions/latest, then pulled into the relevant steps with secretEnv:.
  8. Parameterized configs use _PROJECT_ID + _FUNCTION-style substitutions and validate them at the top of the step body (if [[ -z "${_FUNCTION}" ]]; then exit 1; fi).
  9. .gcloudignore keeps .git, .beads/, and other developer chaff out of the upload tarball.

References

FileWhen to open
references/RECIPES.mdLong-form recipes: Cloud Run + Cloud Run Jobs deploy, Terraform-gated approval pipelines, GitOps with trigger YAML, blue/green via revision tags
references/PITFALLS.mdDeeper troubleshooting catalog with reproducers

Official docs (always-fresh, prefer over recall):

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.