Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
32a9abc
feat(teams): Updated and Added models for List, Create and Update opt…
isivaselvan Mar 16, 2026
6c9a015
feat(teams): Added List method to the teams resource
isivaselvan Mar 16, 2026
308139b
feat(teams): Added create method for team resources
isivaselvan Mar 17, 2026
f8dda5e
feat(teams): Added update method for the team resource
isivaselvan Mar 17, 2026
cf818e8
feat(teams): Added read method for team resource
isivaselvan Mar 17, 2026
efa5203
feat(teams): Added delete method for team resource
isivaselvan Mar 17, 2026
72bb761
feat(teams): Added unit test cases for teams resource
isivaselvan Mar 17, 2026
b811f60
feat(team-project-access): Added models for the team-project-access
isivaselvan Mar 17, 2026
cd218a3
feat(team-project-access): Added resource and examles file
isivaselvan Mar 20, 2026
446f6db
feat(team-project-access): Added models for the team project access r…
isivaselvan Apr 14, 2026
393f0ec
feat(team-project-access): Added list, remove, add, update and read m…
isivaselvan Apr 14, 2026
333a68b
feat(team-project-access): Added invalid team project access id error…
isivaselvan Apr 14, 2026
c6fb4fc
feat(team-project-access): Added examples for the team project access…
isivaselvan Apr 14, 2026
eb2f2db
feat(team-project-access): Added unit testcases for the team project …
isivaselvan Apr 14, 2026
05f2543
feat(stacks): Created models and resource for Stack resource
isivaselvan Apr 16, 2026
b0d25d5
feat(stacks): Added examples for stack resource
isivaselvan Apr 16, 2026
f3e8e00
feat(stacks): Added unit testcases for stack resource
isivaselvan Apr 16, 2026
0cce22b
feat(stacks): Fixed fmt and lints
isivaselvan Apr 16, 2026
2bd04d5
feat(explorer): add explorer support for HCP/TFE 🚀
jasodeep Apr 24, 2026
b95204c
:wrench: explorer fixes
jasodeep Apr 27, 2026
827a738
Improved overall test coverage for Explorer 🔍✨
jasodeep Apr 28, 2026
32b4343
♻️ Refactor Explorer codebase for improved quality and maintainability
jasodeep Apr 28, 2026
4d3cce2
📚✨Improved docstrings coverage better clarity and maintainability
jasodeep Apr 28, 2026
970fe63
Merge pull request #118 from hashicorp/feature/teams
isivaselvan Apr 30, 2026
bd11baa
Merge branch 'next-1.0.0' into feature/team-project-access
isivaselvan Apr 30, 2026
7e21b71
Fixed the fmt and lint
isivaselvan May 4, 2026
0bcfc6b
Merge pull request #127 from hashicorp/feature/team-project-access
isivaselvan May 4, 2026
926bc7a
Merge pull request #128 from hashicorp/feature/stack
isivaselvan May 4, 2026
d420fd2
Add organization token support with models, resources, examples, and …
NimishaShrivastava-dev May 5, 2026
6ac8ec6
feat: update organization token APIs and related files
NimishaShrivastava-dev May 5, 2026
4c6dba7
Merge pull request #141 from NimishaShrivastava-dev/feature/org-token…
isivaselvan May 6, 2026
be0394f
Merge pull request #142 from hashicorp/main
isivaselvan May 6, 2026
d0b826c
Add user API support and current-user endpoints
TanyaSingh369-svg May 6, 2026
6b926b6
feat(registry-provider-platform): Added the errors and models for the…
isivaselvan May 7, 2026
8f1c80a
feat(registry-provider-platform): Added create, list, read and delete…
isivaselvan May 7, 2026
809a77e
feat(registry-provider): Removed the validate function, utilized mode…
isivaselvan May 7, 2026
32c3e94
feat(registry-provider-version): Removed the validate function, updat…
isivaselvan May 7, 2026
598d4d0
feat(registry-provider-platform): Added and updated unit testcases of…
isivaselvan May 7, 2026
436ade5
feat(registry-provider-platform): Added example file for the feature
isivaselvan May 7, 2026
99c6e7a
Fixed lint and fmt
isivaselvan May 7, 2026
f6bf013
Merge pull request #144 from hashicorp/feature/User
isivaselvan May 7, 2026
26ff34e
Merge pull request #145 from hashicorp/feature/registry-provider-plat…
isivaselvan May 7, 2026
c82b454
feat: add organization tags API (list, add_workspaces, delete) with m…
NimishaShrivastava-dev May 7, 2026
a006562
test: update unit tests for organization tags
NimishaShrivastava-dev May 7, 2026
a468c75
Merge branch 'next-1.0.0' into feature/org-tags-api-specs
NimishaShrivastava-dev May 7, 2026
cec2d8b
fix: apply ruff formatting
NimishaShrivastava-dev May 7, 2026
c02c836
client.py is modified
NimishaShrivastava-dev May 7, 2026
3b05117
fixing lint issues
NimishaShrivastava-dev May 7, 2026
044ba21
feat(stack-config): Added models for the stack-config
isivaselvan May 8, 2026
1c8c5af
feat(stack-config): Added create, list and read method for the resource
isivaselvan May 8, 2026
9307768
feat(stack-config): Added examples and unit testcases
isivaselvan May 8, 2026
64df6d4
feat(stack): Added fetch latest stack from vcs method in the stack an…
isivaselvan May 8, 2026
e2032c7
feat(stack-config): updated models and example for stack config
isivaselvan May 8, 2026
4630aa6
self.Users is added
NimishaShrivastava-dev May 8, 2026
518455e
self.Users is added again
NimishaShrivastava-dev May 8, 2026
0abad7b
Add organization audit configuration API support
NimishaShrivastava-dev May 11, 2026
1d6c5a5
fix: apply ruff formatting
NimishaShrivastava-dev May 12, 2026
15c21de
fix: add alias for request_id field
NimishaShrivastava-dev May 12, 2026
056d3ab
fix: enable populate_by_name for request_id alias
NimishaShrivastava-dev May 12, 2026
fac1e9d
Merge pull request #147 from hashicorp/feature/stack-configuration
isivaselvan May 12, 2026
9900d94
feat(comment): Updated models for comments and created errors
isivaselvan May 12, 2026
f1f6a77
feat(comment): Added list, read and create methods for comment feature
isivaselvan May 12, 2026
962b178
feat(comment): Added unit tests and examples files
isivaselvan May 12, 2026
1555ed8
feat: add Task Result API support with example and unit tests
TanyaSingh369-svg May 12, 2026
7758321
Merge branch 'next-1.0.0' into feature/task-result-api
TanyaSingh369-svg May 12, 2026
9426182
fix: address lint issues in task result implementation
TanyaSingh369-svg May 12, 2026
9558c83
Merge branch 'next-1.0.0' into main
isivaselvan May 13, 2026
d15ba17
Merge pull request #136 from jasodeep/main
isivaselvan May 13, 2026
29ee119
Merge pull request #155 from hashicorp/feature/comments
isivaselvan May 14, 2026
6df751c
Merge branch 'next-1.0.0' into feature/organization-audit-configurati…
NimishaShrivastava-dev May 14, 2026
00afd13
refactor organization tags list API to iterator pattern
NimishaShrivastava-dev May 14, 2026
6c9f838
format organization tags files
NimishaShrivastava-dev May 14, 2026
70d25f9
move organization tag errors to shared errors module
NimishaShrivastava-dev May 14, 2026
528ea2a
feat(team-token): Added models for the team token resource
isivaselvan May 15, 2026
7008646
feat(team-token): Added list, read, create and delete methods for bot…
isivaselvan May 15, 2026
45b5f1b
feat(team-token): Added examples and unit tests for team token
isivaselvan May 15, 2026
ab076fb
Fixed links and fmt checks
isivaselvan May 15, 2026
180db0d
fix: map task stage relationship in task result resource
TanyaSingh369-svg May 18, 2026
31d1486
feat(terraform-actions): Added invoke action address to Run and RunCr…
isivaselvan May 18, 2026
6627c1a
feat(terraform-actions): Updated examples with invoke-action flag for…
isivaselvan May 18, 2026
20390c9
Add Run Task integration callback support
TanyaSingh369-svg May 19, 2026
3e830e7
Improve Run Task integration examples and callback flow
TanyaSingh369-svg May 20, 2026
d38d548
Merge pull request #146 from NimishaShrivastava-dev/feature/org-tags-…
isivaselvan May 21, 2026
e116ac3
Bump pytfe project version from 0.1.5 to 1.0.0
iam404 May 22, 2026
f205289
Merge pull request #157 from hashicorp/feature/team-tokens
isivaselvan May 22, 2026
133c712
Merge pull request #158 from hashicorp/feature/terraform-actions
isivaselvan May 22, 2026
407e0a8
fix: export organization audit configuration models
NimishaShrivastava-dev May 22, 2026
edd52d7
fix: map task result relationships into typed SDK models
TanyaSingh369-svg May 22, 2026
ee659f2
Merge branch 'next-1.0.0' into feature/task-result-api
TanyaSingh369-svg May 22, 2026
a7afebe
Add run task integration model exports
TanyaSingh369-svg May 22, 2026
1e039d4
updated resources
NimishaShrivastava-dev May 22, 2026
43f3fd8
Merge pull request #154 from NimishaShrivastava-dev/feature/organizat…
isivaselvan May 22, 2026
561f520
Merge pull request #160 from hashicorp/feature/run-task-integration-api
isivaselvan May 22, 2026
3ba2720
Merge branch 'next-1.0.0' into feature/task-result-api
isivaselvan May 22, 2026
17fe314
Merge pull request #156 from hashicorp/feature/task-result-api
isivaselvan May 22, 2026
d5e76bd
feat: add workspace run task resource and models
isivaselvan May 23, 2026
f8f7627
feat(task-stage): add TaskStage resource and models
isivaselvan May 23, 2026
13fb423
moved test_task_stage unit tests into units folder
isivaselvan May 23, 2026
d0bd550
Merge pull request #164 from hashicorp/feature/workspace-run-task
isivaselvan May 23, 2026
51d026d
Merge pull request #165 from hashicorp/feature/task-stage
isivaselvan May 23, 2026
173bec5
State version upload (#163)
NimishaShrivastava-dev May 23, 2026
9666b98
Fix Auth leak in state version upload
iam404 May 23, 2026
111012e
refactor(agent-pool): Updated models to include project_ids, workspac…
isivaselvan May 23, 2026
86e96f7
refactor(agent-pool): Added assign_to_project method to the agent-poo…
isivaselvan May 23, 2026
4597aa5
refactor(agent-pool): added agent-pool errors, added models in init, …
isivaselvan May 23, 2026
2da9da1
Updated models to handle circular import issues
isivaselvan May 23, 2026
cec7be0
fixed fmt and lint
isivaselvan May 23, 2026
f45bc4d
Merge pull request #166 from hashicorp/refactor/agentpool-models
isivaselvan May 23, 2026
a6ce901
Fix typos and move enum stage to avoid conflict (#167)
iam404 May 24, 2026
0b67123
Add surrounding features - Agents Documentations with Other features …
iam404 May 24, 2026
c95a3ed
fix and optimise explorer (#170)
iam404 May 24, 2026
5637c74
Add logger for the sdk (#171)
iam404 May 24, 2026
02c6b52
fixed import issues
NimishaShrivastava-dev May 25, 2026
56eb90e
Merge pull request #172 from NimishaShrivastava-dev/fix-organization-…
isivaselvan May 25, 2026
5d7cec1
Updated Changelog
isivaselvan May 25, 2026
6c2497a
upd changelog
iam404 May 25, 2026
457d27e
Add release workflow to pypi
iam404 May 26, 2026
8591323
Added stack, team-project-access models at init and renamed TaskResul…
isivaselvan May 26, 2026
33e27b8
Documentations - Add scenario, api docs and fix variable_set model (#…
iam404 May 26, 2026
6d180c0
enforce populate_by_name through ci
iam404 May 26, 2026
842ff8e
enforce populate_by_name through ci
iam404 May 26, 2026
be457d0
Updated team model UpdateOption visibility and OrganizationAccess to …
isivaselvan May 27, 2026
02f8737
Add feature no-code-module and bump user-agent (#174)
iam404 May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: release

# Publishes the package to PyPI / TestPyPI via manual dispatch only.
#
# Flow:
# 1. Cut and push a git tag (e.g. `v1.0.0`) on a commit merged into main.
# 2. Actions → release → Run workflow → enter the tag, choose target=testpypi.
# 3. Verify the TestPyPI release looks correct.
# 4. Re-run the workflow with the same tag and target=pypi.
#


on:
workflow_dispatch:
inputs:
target:
description: "Where to publish"
required: true
default: testpypi
type: choice
options: [testpypi, pypi]
tag:
description: "Git tag to publish (e.g. v1.0.0). Must exist on origin and be reachable from main."
required: true

permissions:
contents: read

jobs:
# ---------------------------------------------------------------------------
# 1. Validate that the inputs are sane and the source commit is releasable.
# - tag input is a bare name (no path separators / refspec tricks)
# - the ref we end up on is genuinely a git tag, not a branch with
# the same name (resolved via refs/tags/$TAG)
# - the tag commit is reachable from origin/main (caught even when
# pyproject version was bumped on an un-merged branch)
# - tag string matches pyproject.toml's version
# ---------------------------------------------------------------------------
validate:
name: Validate release source
runs-on: ubuntu-latest
outputs:
tag-ref: ${{ steps.resolve.outputs.tag-ref }}
steps:
- name: Sanitise and qualify the tag input
id: resolve
env:
TAG_INPUT: ${{ inputs.tag }}
run: |
set -euo pipefail
# Reject inputs that look like refspecs, paths, or anything other
# than a bare tag name. Stops attempts like 'heads/main' or
# '../../etc/passwd' or 'refs/tags/v1.0.0' (which would be
# double-prefixed below).
if [[ "$TAG_INPUT" != "$(printf '%s' "$TAG_INPUT" | tr -d '\n')" ]]; then
echo "::error::tag input must not contain newlines."; exit 1
fi
case "$TAG_INPUT" in
""|*/*|*..*|.*|*' '*) echo "::error::tag input '$TAG_INPUT' must be a bare git tag name (no '/', '..', leading '.', or whitespace)."; exit 1 ;;
esac
echo "tag-ref=refs/tags/$TAG_INPUT" >> "$GITHUB_OUTPUT"

- name: Checkout the fully-qualified tag ref
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
# ref must be 'refs/tags/<tag>' (not just '<tag>') so a branch
# with the same name cannot win git's dwim resolution.
ref: ${{ steps.resolve.outputs.tag-ref }}
fetch-depth: 0

- name: Verify HEAD actually points at the requested tag
env:
TAG_INPUT: ${{ inputs.tag }}
run: |
set -euo pipefail
# Belt-and-braces: even though we asked for refs/tags/$TAG,
# confirm git resolved it to a real tag pointing at HEAD.
if ! git tag --points-at HEAD | grep -Fxq "$TAG_INPUT"; then
echo "::error::HEAD does not match tag '$TAG_INPUT'. Checkout resolved to a different ref."
exit 1
fi
echo "HEAD is exactly tag '$TAG_INPUT' (commit $(git rev-parse --short HEAD))"

- name: Verify HEAD is reachable from origin/main
run: |
set -euo pipefail
git fetch --no-tags origin main:origin-main
if ! git merge-base --is-ancestor HEAD origin-main; then
echo "::error::Tag commit is not reachable from origin/main."
echo "::error::This catches tags placed on branches that were never merged, and tags whose commits were later force-pushed off main."
exit 1
fi
echo "tag commit is on main — OK"

- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c
with:
python-version: "3.12"

- name: Verify tag matches pyproject.toml version
env:
TAG_INPUT: ${{ inputs.tag }}
run: |
set -euo pipefail
py_ver=$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_bytes())['project']['version'])")
if [[ "$TAG_INPUT" != "v${py_ver}" && "$TAG_INPUT" != "$py_ver" ]]; then
echo "::error::Tag '$TAG_INPUT' does not match pyproject.toml version '$py_ver' (expected 'v$py_ver' or '$py_ver')."
exit 1
fi
echo "tag '$TAG_INPUT' matches pyproject.toml version '$py_ver'"

# ---------------------------------------------------------------------------
# 2. Run lint + tests across every supported Python version as a release
# gate. Catches "tag exists but main is broken on 3.10" scenarios even
# if branch protection didn't catch it.
# ---------------------------------------------------------------------------
test:
name: Test (py${{ matrix.python-version }})
needs: validate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
ref: ${{ needs.validate.outputs.tag-ref }}

- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c
with:
python-version: ${{ matrix.python-version }}

- name: Install dev deps and run lint + tests
run: |
python -m pip install --upgrade pip
make dev-install
make lint
make test

# ---------------------------------------------------------------------------
# 3. Build and upload. Uses bin/publish-pypi.sh verbatim so the local and
# CI publish paths stay identical (single source of truth for build
# flags, twine invocation, and upload URL selection).
#
# The environment binding (pypi or testpypi) determines which
# PYPI_API_TOKEN secret resolves at runtime; each environment owns
# its own copy. The same is true for required reviewers — configure
# them per environment in repo Settings.
# ---------------------------------------------------------------------------
publish:
name: Publish to ${{ inputs.target }}
needs: [validate, test]
runs-on: ubuntu-latest
environment:
name: ${{ inputs.target }}
# GitHub Environments display URL — distinct from the upload URL,
# which lives in bin/publish-pypi.sh. Point it at the project page
# of whichever index we are publishing to so the "View deployment"
# link in the Actions UI doesn't mislead approvers.
url: ${{ (inputs.target == 'testpypi' && 'https://test.pypi.org/project/pytfe/') || 'https://pypi.org/project/pytfe/' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
ref: ${{ needs.validate.outputs.tag-ref }}

- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c
with:
python-version: "3.12"

- name: Publish via bin/publish-pypi.sh
env:
PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
PYPI_REPO: ${{ inputs.target }}
run: bash bin/publish-pypi.sh
139 changes: 139 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# AGENTS.md — guide for AI agents working in this repository

If you are an AI coding agent (Claude Code, Codex, Cursor, GitHub Copilot Workspace, etc.) about to make changes to this repository, read this file first. It will save you from generating code that diverges from the codebase's conventions.

If you are a human contributor, the same conventions apply to you — but the more comprehensive [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) and the deep-dive references linked below are written for you specifically.

## What this repo is

`pytfe` is the official Python SDK for the HCP Terraform and Terraform Enterprise V2 API. It wraps roughly 50 resource services (workspaces, runs, policies, teams, agents, …) and is consumed by downstream projects. Source layout:

```
src/pytfe/
client.py # TFEClient — composition root, wires every resource
config.py # TFEConfig — auth, timeout, retry, proxy settings
_http.py # HTTPTransport — request, retry, redirects, auth
_jsonapi.py # JSON:API envelope helpers
_logging.py. # Logging primitives for the pytfe SDK
errors.py # Typed exception hierarchy (TFEError + ~80 subclasses)
utils.py # Validation + small helpers
models/ # Pydantic v2 models, one file per resource
resources/ # Service classes, one file per resource

tests/units/ # Pytest unit tests with mocked transport, one file per resource
examples/ # Runnable CLI demos, one file per resource (or extended)
docs/ # Internal reference (see below)
```

## Required reading before generating code

These three documents define the patterns this codebase already uses. Generating code without consulting them will produce inconsistent output:

| Topic | Doc |
|---|---|
| `list_*` methods, pagination, iterator vs list, the `_list` helper | [`docs/ITERATORS.md`](docs/ITERATORS.md) |
| Pydantic model conventions: `ConfigDict`, aliases, validators, relationships, exporting | [`docs/MODELS.md`](docs/MODELS.md) |
| Resource service patterns: method shape, JSON:API envelopes, client wiring, examples | [`docs/RESOURCE.md`](docs/RESOURCE.md) |
| Logging: namespace, redaction, env-var setup, debug round-trip traces | [`docs/LOGGING.md`](docs/LOGGING.md) |

Each doc ends with a checklist. Use those checklists; they encode the rules a reviewer will look for.

## Source verification for API shape

Before adding a new resource, endpoint, enum, or non-obvious response parser, verify the wire contract against primary sources:

- Official HCP Terraform API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs
- go-tfe implementation: https://github.com/hashicorp/go-tfe
- OpenAPI specs or live probes when the public docs/go-tfe are missing, beta, or ambiguous

Use the official docs and go-tfe as the first sources of truth. OpenAPI and live probes are supporting evidence, especially for endpoints that are newly released or not fully documented yet. When behavior is surprising, note the source you checked in the PR description, test name, example header, or a short code comment.

## The cardinal rules

A handful of conventions are pervasive enough that you'll regret breaking them. In rough order of "how loudly it breaks at review time":

1. **`list_*` methods return `Iterator[X]`.** Not `list[X]`, not `Iterable[X]`, not a custom `Pager`. Use `for item in self._list(path): yield ...` inside the method body. ([ITERATORS.md](docs/ITERATORS.md))

2. **JSON:API attribute names go through `Field(alias="...")`.** The API sends `created-at`, Python uses `created_at`. Pair with `model_config = ConfigDict(populate_by_name=True, validate_by_name=True)` on the model. ([MODELS.md](docs/MODELS.md))

3. **`model_dump(by_alias=True, exclude_none=True)` for write payloads.** Without `by_alias=True` you'll send snake_case to the API and it will silently drop the fields. Add `mode="json"` if the options contain enums.

4. **For new public APIs, prefer typed `TFEError` subclasses.** The error hierarchy in `errors.py` is part of the public API, and downstream consumers often `except TFEError:` once. Existing methods still expose many `ValueError` paths; do not change those established exceptions unless the breaking-change impact is explicitly accepted.

5. **Validate IDs at the top of every method.** Use `valid_string_id` from `utils.py`. New methods should prefer typed `Invalid<Thing>IDError` errors; existing resources may already use `ValueError` and should keep that public behavior unless a breaking change is intentional.

6. **Wire every new resource into `client.py`.** A resource not added to `TFEClient.__init__` is unreachable. Same for new models in `models/__init__.py`.

7. **Use the standard verb names: `list`, `read`, `create`, `update`, `delete`.** Plus `add_*` / `remove_*` for relationship modifications. Argument order is always *identifiers first, options last*.

## Things that look reasonable but are actually wrong here

These are mistakes a competent Python developer would make if they hadn't read the conventions. Avoid them:

- **Don't catch `httpx` errors directly.** The transport already translates them into `TFEError` subclasses. Catching `httpx.HTTPError` in a resource means the typed error never propagates.
- **Always send the bearer token, even to absolute URLs returned by the API.** Endpoints like `hosted_state_download_url`, `hosted_state_upload_url`, plan `json-output`, and apply `errored-state` redirect to `archivist.terraform.io` — which is HashiCorp infrastructure that *requires* the bearer. go-tfe does the same (see `state_version.go::Download` + `tfe.go::NewRequest`). Stripping the bearer breaks downstream consumers (notably the Ansible collection's statefile + dynamic-inventory flows). `HTTPTransport.request` accepts `include_auth=False` only as an opt-out for the hypothetical case of calling a genuinely non-HashiCorp host; do not use it for Archivist URLs.
- **Don't write a custom page loop.** `self._list(path, params=...)` handles pagination + non-paginated endpoints transparently. Rolling your own loop will diverge from the rest of the codebase.
- **Don't reuse generators.** Iterators returned by `list_*` are single-use. If you need to traverse twice, `materialized = list(client.foo.list_bars(...))` first.
- **Don't add features beyond what was asked.** This codebase is approaching v1.0.0. Adding "while I'm here" refactors or speculative abstractions slows reviews and risks breaking the Ansible collection.
- **Don't assume every successful response is `{"data": ...}`.** Check the docs/go-tfe/spec for each endpoint: some return a JSON:API envelope, some return a bare resource object, `204 No Content`, `null`, raw bytes, or a redirect to a blob URL. Add tests for non-standard shapes.
- **Don't use bare `list[...]` annotations inside a resource class after defining `def list(...)`.** In class scope, mypy can resolve `list` to the method instead of the builtin. Use `builtins.list[...]`, `Sequence[...]`, or another unshadowed type.
- **Don't `print()` or use ad-hoc `logging.getLogger(__name__)` calls in library code.** The SDK has a structured logging framework — use `pytfe._logging.transport_logger` for HTTP traffic, or `pytfe._logging.logger` (the `pytfe` root) for higher-level events. Everything from that namespace is silent by default (NullHandler) and respects the user's `setup_logging()` or stdlib configuration. See [LOGGING.md](docs/LOGGING.md) for redaction rules — bearer tokens and `token`/`secret`/`password` keys are auto-redacted by `RoundTrip`, but only inside that formatter. Never `log.info(token)` directly.

## Known cross-dependencies you should not break

| Consumer | What they depend on |
|---|---|
| `hashicorp/terraform-ansible-collection` | The pytfe public API — resource methods, model fields, exception classes. Any signature change here is a breaking change. In particular, `client.projects.list_tag_bindings` is consumed with an `isinstance(response, list)` check; it intentionally still returns `list[TagBinding]` (see [ITERATORS.md](docs/ITERATORS.md) — Known exceptions). |
| Downstream user code generally | Method signatures, return types, model fields, and exception types. New errors should subclass an existing parent so `except TFEError:` continues to work, but existing `ValueError` behavior should not be changed casually. |

When in doubt about whether a change is breaking: check `gh search code '<symbol>' --owner hashicorp` to see if the Ansible repo uses it.

## How to make a change

This is the workflow that produces low-friction reviews. Follow it.

1. **Understand the scope first.** If the task is "add resource X", read `docs/RESOURCE.md` end-to-end. If it's "fix bug in Y", read `Y`'s current implementation and tests before touching anything.
2. **Check official API docs and go-tfe for the canonical API shape.** `pytfe` mirrors the HCP Terraform API and often follows go-tfe's surface. URL paths, method names, payload shapes, response shapes, enum values, and redirect behavior should be verified against https://developer.hashicorp.com/terraform/cloud-docs/api-docs and https://github.com/hashicorp/go-tfe before designing anything.
3. **Add models first** (`src/pytfe/models/<resource>.py`), then the resource (`src/pytfe/resources/<resource>.py`), then wire both into the respective `__init__.py` / `client.py`.
4. **Write tests.** Mock `HTTPTransport`. One test per method, plus an invalid-id case for every public method. See `tests/units/test_comment.py` as a small reference.
5. **Run `make test` and `make lint`.** Both must pass. `pytest tests/units/` runs the suite directly; it should be < 2 seconds.
6. **Add or extend an example.** Real engineers will copy-paste it; make it work end-to-end. Use env vars (`TFE_TOKEN`, `TFE_ORG`) for auth, never hard-code credentials.
7. **If the change is non-trivial, verify live.** The repo doesn't run integration tests in CI, so the only way to catch a wrong URL or a typo in an attribute alias is to run the example against a real organization.

## Things to never do

- **Never put a token, password, or other credential in any file.** Use environment variables. The user will rotate them after; you don't need to know them.
- **Never use `git push --force` or `git reset --hard` without explicit instruction.** Same for `--no-verify`, force-push to `main`, or rebasing public commits.
- **Never commit `.env`, `credentials.json`, `*.tfstate`, or anything with secrets.** Match against the existing `.gitignore` if unsure.
- **Never bypass pre-commit hooks.** If a hook fails, fix the underlying issue.
- **Never run an example that creates real resources against production without explicit user confirmation.** Sandbox orgs are safe; user's actual workspace is not.

## Style

The codebase uses [ruff](https://docs.astral.sh/ruff/) for both formatting and linting and [mypy](https://mypy.readthedocs.io/) for type checking. Type hints are required on every public method's signature. Docstrings are required on every public method — keep them to one or two lines unless the behavior is genuinely non-obvious.

Comments are minimal by design. A comment should explain *why* something non-obvious is true, not *what* the code does. The names and types should be enough to convey "what".

```python
# ❌ Don't
# Increment the counter by 1
counter += 1

# ✅ Do (only when the why is non-obvious)
# Run task stages are wire values, not Python names. If the API/go-tfe says
# "pre-plan", keep the hyphen; do not "normalize" it to snake_case.
stage_value = raw_value
```

## When you're done

A reasonable PR includes:

- Code (resource + models)
- Tests covering every public method
- An updated or new example
- A short `CHANGELOG.md` entry under `# v<next-minor>.0 (Unreleased)` describing the user-visible change

Open the PR with a description that explains *why* the change is needed, links to any HCP Terraform API docs or go-tfe code referenced, and notes any behavior changes a downstream consumer might see.

The reviewer's checklist will be the union of the checklists in [`docs/ITERATORS.md`](docs/ITERATORS.md), [`docs/MODELS.md`](docs/MODELS.md), and [`docs/RESOURCE.md`](docs/RESOURCE.md). Pre-running them yourself is the fastest way to a merge.
Loading
Loading