Release a state lock a killed job left
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
Read https://intentius.io/terragucci/guides/release-a-state-lock/.
A plan or apply in this repo fails because the state of a root I name is locked. Read the lock for that root (a lock file, a gcs lock object, or an azurerm blob lease) and tell me its ID, who took it and when, and which of the forge's runs that began before then are still running.
Do not run `terragucci unlock-state`, `force-unlock` or any state command, and do not delete the lock file.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.Result
Section titled “Result”When a runner kills a job mid-apply (a timeout or a cancelled run), the binary never releases its hold on the state. The root’s next plan then waits until its timeout and fails. terragucci unlock-state runs force-unlock with checks a hand run lacks:
| Check | What it guards against |
|---|---|
| no run that began before it was taken is alive | freeing state that a job still applying holds, so that two applies write one state |
| an approval of its ID | a release only the person typing agreed to, or one of a newer hold taken since |
a record on chant/lifecycle |
a release nobody can find later; the audit trail lists it as unlock |
It reads the lock where the backend keeps it:
| Backend | Lock | ID |
|---|---|---|
s3 with use_lockfile = true |
the lock file <key>.tflock |
the ID in the file |
gcs |
the lock object <prefix>/<workspace>.tflock |
the object’s generation, as the binary prints it |
azurerm |
a lease on the state blob, with the lock info in its metadata | the lease’s ID |
| GitLab-managed state | GitLab’s lock on the state | the ID in the lock |
A comment never runs it. With binary: choudoufu and a record store, a killed job holds no lock, so there is none to release (locking with choudoufu).
Free the state
Section titled “Free the state”-
Find the root. The failed job’s log names the state and the ID:
Error: Error acquiring the state lockLock Info:ID: 1f0c6a52-...Path: acme-state/envs/prod/app.tfstateOperation: OperationTypeApplyWho: root@runner-7Created: 2026-10-09 18:02:11.420 +0000 UTC -
Make sure the job that took it has ended, and stop it on the forge if not.
-
In a checkout of the default branch, with the backend’s credentials and the forge token in the variable
token_envnames (FORGEJO_TOKEN,GITHUB_TOKENorGITLAB_TOKENby default), run:Terminal window npx terragucci unlock-state envs/prod/appA run that began before the hold and is still running may be the holder; then nothing is freed, and the command exits 1 naming the run. With no such run, it records the release as waiting and prints the command that approves it:
terragucci unlock-state: envs/prod/app: releasing lock 1f0c6a52-..., OperationTypeApply by root@runner-7 at 2026-10-09T18:02:11.420Z waits for an approval of digest jcs1-sha256:6d2b.... Approve it with:terragucci unlock-state: terragucci approve unlock envs/prod/app --plan jcs1-sha256:6d2b... -
Whoever approves your waves runs that command, with
--signunderapproval: sealed. The approval names this ID and no other. -
Run
npx terragucci unlock-state envs/prod/appagain. It asks the forge about the runs once more and frees the state with the binary’sforce-unlockof that ID. The release goes in_gates/tf-unlock/done.jsonlwith the approver and the person who ran it. -
Rerun the wave or push the next change; its plan takes the state as usual.
GitLab-managed state
Section titled “GitLab-managed state”GitLab has no call that reads a lock, so unlock-state asks for it. A held lock answers with its ID, the GitLab user who took it and when. A free lock is taken and given back at once. The release is a DELETE of that ID on the lock endpoint, which GitLab refuses when another lock holds the state.
| You need | Why |
|---|---|
lock_address and unlock_address in the block, or TF_HTTP_LOCK_ADDRESS and TF_HTTP_UNLOCK_ADDRESS |
without them the backend takes no lock |
TF_HTTP_USERNAME, and a personal access token with the Maintainer role in TF_HTTP_PASSWORD |
GitLab lets Maintainers lock and unlock state |
The log names the lock endpoint:
terragucci unlock-state: app: https://gitlab.example.com/api/v4/projects/42/terraform/state/app/lock holds lock 1f0c... by deploy-bot at 2026-10-09T18:02:11.420ZFlags and exit codes are on CLI commands.
These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.