Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/etc/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ allstar
anchore
buildx
conftest
connectionid
cookiecutter
dependabot
digestabot
Expand Down
13 changes: 11 additions & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
"github_org": "zenable-io",
"project_owner_github_username": "jonzeolla",
"python_version": ["3.13", "3.12", "3.11"],
"dockerhub": ["no", "yes"],
"dockerhub_subscription": ["none", "personal", "team", "business"],
"public": ["no", "yes"],
"license": ["NONE", "MIT", "BSD-3-Clause"]
"license": ["NONE", "MIT", "BSD-3-Clause"],
"__prompts__": {
"dockerhub_subscription": {
"__prompt__": "Select your Docker Hub subscription; choose none to disable Docker Hub publishing",
"none": "None — disable Docker Hub publishing",
"personal": "Personal — authenticate with username and PAT",
"team": "Team — authenticate with OIDC",
"business": "Business — authenticate with OIDC"
}
}
}
25 changes: 14 additions & 11 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ When generating a new project, you'll be prompted for the following configuratio

### Technical Options

| Variable | Description | Default | Options |
| ---------------- | ---------------------------- | ------- | ----------------------------- |
| `python_version` | Minimum Python version | "3.13" | "3.11", "3.12", "3.13" |
| `dockerhub` | Enable Docker Hub publishing | "no" | "yes", "no" |
| `public` | Make repository public | "yes" | "yes", "no" |
| `license` | Project license | "NONE" | "NONE", "MIT", "BSD-3-Clause" |
| Variable | Description | Default | Options |
| ------------------------ | --------------------------------------------------- | ------- | ---------------------------------------- |
| `python_version` | Minimum Python version | "3.13" | "3.11", "3.12", "3.13" |
| `dockerhub_subscription` | Docker Hub plan; `none` disables image publishing | "none" | "none", "personal", "team", "business" |
| `public` | Make repository public | "yes" | "yes", "no" |
| `license` | Project license | "NONE" | "NONE", "MIT", "BSD-3-Clause" |

## Post-Generation Configuration

Expand Down Expand Up @@ -67,11 +67,14 @@ vars:

For detailed information about pre-commit hooks configuration and available hooks, see the [Hooks Guide](hooks.md#pre-commit-hooks).

#### Docker Hub Secrets
#### Docker Hub Authentication

If you enabled Docker Hub publishing:
The `dockerhub_subscription` choice controls publishing and authentication:

- `DOCKERHUB_USERNAME`
- `DOCKERHUB_PAT`
- **None:** Docker Hub publishing is not generated.
- **Personal:** Add `DOCKERHUB_USERNAME` and `DOCKERHUB_PAT` as GitHub Actions secrets.
- **Team or Business:** [Create a Docker Hub OIDC connection](https://docs.docker.com/enterprise/security/oidc-connections/create-manage/) whose ruleset grants the
generated repository write access. Add `DOCKERHUB_ORGANIZATION` and `DOCKERHUB_OIDC_CONNECTIONID` as GitHub Actions variables.

A reminder to set this is also printed after project generation if you answered "yes" to the docker hub question.
Personal subscriptions use the standard Docker Hub login. Team and Business subscriptions exchange the GitHub identity token for a short-lived Docker Hub token.
The setup reminder describes only the selected authentication path.
32 changes: 16 additions & 16 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,21 @@ def write_context(*, context: dict) -> None:
yaml.dump(context, file)


def notify_dockerhub_secrets() -> None:
"""Notify user about required Docker Hub secrets for releases."""
# We no longer need this once https://github.com/docker/roadmap/issues/314 is available
def notify_dockerhub_configuration(*, use_oidc: bool) -> None:
"""Notify user about the selected Docker Hub authentication configuration."""
print("\n" + "=" * 70)
print("IMPORTANT: Docker Hub Publishing Enabled")
print("=" * 70)
print("\nYou have enabled Docker Hub publishing for releases")
print("Please ensure the following GitHub secrets are configured:")
print("\n • DOCKERHUB_USERNAME - Your Docker Hub username")
print(" • DOCKERHUB_PAT - Your Docker Hub Personal Access Token")
print("\nWithout these secrets, your releases will fail during the")
print("Docker image publishing step")
print("\nTo add these secrets:")
print("1. Go to your GitHub repository settings")
print("2. Navigate to Settings → Secrets and variables → Actions")
print("3. Add the required secrets")
if use_oidc:
print("\nConfigure a Docker Hub OIDC connection for this GitHub repository.")
print("Then add these GitHub Actions variables:")
print("\nDOCKERHUB_ORGANIZATION - Your Docker Hub organization name")
print(" • DOCKERHUB_OIDC_CONNECTIONID - The Docker Hub OIDC connection ID")
print("\nReleases use short-lived Docker Hub tokens.")
else:
print("\nAdd these GitHub Actions secrets:")
print("\n • DOCKERHUB_USERNAME - Your Docker Hub username")
print(" • DOCKERHUB_PAT - Your Docker Hub Personal Access Token")
print("=" * 70 + "\n")


Expand Down Expand Up @@ -428,9 +427,10 @@ def run_post_gen_hook():
# (i.e. check=False)
subprocess.run(["task", "init"], check=False, capture_output=True)

# Notify about Docker Hub secrets if Docker Hub publishing is enabled
if cookiecutter_context.get("dockerhub") == "yes":
notify_dockerhub_secrets()
# Notify about Docker Hub authentication if publishing is enabled
dockerhub_subscription = cookiecutter_context.get("dockerhub_subscription")
if dockerhub_subscription != "none":
notify_dockerhub_configuration(use_oidc=dockerhub_subscription in {"team", "business"})
except subprocess.CalledProcessError as error:
stdout = error.stdout.decode("utf-8") if error.stdout else "No stdout"
stderr = error.stderr.decode("utf-8") if error.stderr else "No stderr"
Expand Down
50 changes: 50 additions & 0 deletions tests/test_cookiecutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def render_config(*, config: dict) -> dict:
"""Render the provided config"""
rendered_config: dict[str, str | list] = {}
for key, value in config.items():
if key.startswith("__"):
continue
if isinstance(value, str):
# Sanitize by removing the "cookiecutter." prefix
sanitized_template = value.replace("cookiecutter.", "")
Expand Down Expand Up @@ -126,6 +128,54 @@ def test_supported_options(cookies, context_override):
check_files(files)


@pytest.mark.unit
def test_dockerhub_subscription_prompt():
"""Offer one Docker Hub subscription question that can disable publishing."""
config = get_config()

assert config["dockerhub_subscription"] == ["none", "personal", "team", "business"]
prompt = config["__prompts__"]["dockerhub_subscription"]
assert prompt["__prompt__"] == "Select your Docker Hub subscription; choose none to disable Docker Hub publishing"
assert prompt["none"] == "None — disable Docker Hub publishing"


@pytest.mark.unit
@pytest.mark.parametrize("dockerhub_subscription", ["none", "personal", "team", "business"])
def test_dockerhub_release_authentication(cookies, dockerhub_subscription):
"""Render the selected Docker Hub authentication path."""
os.environ["RUN_POST_HOOK"] = "false"

result = cookies.bake(extra_context={"dockerhub_subscription": dockerhub_subscription})

assert result.exit_code == 0
release_workflow = (result.project_path / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
taskfile = (result.project_path / "Taskfile.yml").read_text(encoding="utf-8")

if dockerhub_subscription == "none":
assert "publish-docker:" not in release_workflow
assert "\n publish:\n" not in taskfile
elif dockerhub_subscription in {"team", "business"}:
assert "\n publish:\n" in taskfile
assert "uses: docker/setup-qemu-action@v3\n\n - name: Login to Docker Hub" in release_workflow
assert "id-token: write" in release_workflow
assert "uses: docker/login-action@v4\n env:" in release_workflow
assert "DOCKERHUB_OIDC_CONNECTIONID: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}" in release_workflow
assert (
"username: ${{ vars.DOCKERHUB_ORGANIZATION }}\n\n - name: Build and publish multiplatform Docker image"
) in release_workflow
assert "DOCKERHUB_PAT" not in release_workflow
else:
assert "\n publish:\n" in taskfile
assert "uses: docker/setup-qemu-action@v3\n\n - name: Login to Docker Hub" in release_workflow
assert "id-token: write" not in release_workflow
assert "uses: docker/login-action@v4\n with:" in release_workflow
assert "username: ${{ secrets.DOCKERHUB_USERNAME }}" in release_workflow
assert (
"password: ${{ secrets.DOCKERHUB_PAT }}\n\n - name: Build and publish multiplatform Docker image"
) in release_workflow
assert "DOCKERHUB_OIDC_CONNECTIONID" not in release_workflow


@pytest.mark.integration
def test_update(cookies):
"""
Expand Down
13 changes: 10 additions & 3 deletions {{cookiecutter.project_name}}/.github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ jobs:
TAG=$(git describe --tags --abbrev=0)
echo "tag=${TAG}" | tee -a "${GITHUB_OUTPUT}"
echo "Created release tag: ${TAG}"
{%- if cookiecutter.dockerhub == 'yes' %}
{%- if cookiecutter.dockerhub_subscription != 'none' %}

publish-docker:
name: Publish Docker Image
needs: release
runs-on: ubuntu-24.04
permissions:
contents: read{% if cookiecutter.dockerhub_subscription in ['team', 'business'] %}
id-token: write{% endif %}
steps:
- name: Checkout the repository
uses: actions/checkout@v6
Expand All @@ -72,10 +75,14 @@ jobs:
uses: docker/setup-qemu-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4{% if cookiecutter.dockerhub_subscription in ['team', 'business'] %}
env:
DOCKERHUB_OIDC_CONNECTIONID: ${{ "{{ vars.DOCKERHUB_OIDC_CONNECTIONID }}" }}
with:
username: ${{ "{{ vars.DOCKERHUB_ORGANIZATION }}" }}{% else %}
with:
username: ${{ "{{ secrets.DOCKERHUB_USERNAME }}" }}
password: ${{ "{{ secrets.DOCKERHUB_PAT }}" }}
password: ${{ "{{ secrets.DOCKERHUB_PAT }}" }}{% endif %}

- name: Build and publish multiplatform Docker image
run: |
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.project_name}}/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ tasks:
as: tool
cmd: '{{ '{{if eq .FORCE_LINK "true"}}' }}brew link --force {{ '{{.tool}}' }}{{ '{{else}}' }}true{{ '{{end}}' }}'

{%- if cookiecutter.dockerhub == 'yes' %}
{%- if cookiecutter.dockerhub_subscription != 'none' %}

publish:
desc: Publish the project artifacts; docker images, compiled binaries, etc.
Expand Down
Loading