From 2fd8e23afe4502c06b917a5f9c1b25680b61dcdf Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Sun, 25 Oct 2026 21:28:04 +0900 Subject: [PATCH 1/4] feat(aws.codeartifact.configure-credentials): add package-manager-independent authentication --- .../README.md | 115 ++++++++++++++++++ .../action.yaml | 96 +++++++++++++++ .../tests/mock-aws.py | 24 ++++ .../tests/test_action.py | 106 ++++++++++++++++ ...deartifact.configure-credentials.test.yaml | 74 +++++++++++ 5 files changed, 415 insertions(+) create mode 100644 .github/actions/aws.codeartifact.configure-credentials/README.md create mode 100644 .github/actions/aws.codeartifact.configure-credentials/action.yaml create mode 100644 .github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py create mode 100644 .github/actions/aws.codeartifact.configure-credentials/tests/test_action.py create mode 100644 .github/workflows/aws.codeartifact.configure-credentials.test.yaml diff --git a/.github/actions/aws.codeartifact.configure-credentials/README.md b/.github/actions/aws.codeartifact.configure-credentials/README.md new file mode 100644 index 0000000..d5a0cb2 --- /dev/null +++ b/.github/actions/aws.codeartifact.configure-credentials/README.md @@ -0,0 +1,115 @@ +# AWS - CodeArtifact Configure Credentials + +Fetch a CodeArtifact authorization token and a repository endpoint using the job's +existing AWS credentials. Supports Python (`pypi`), Node.js (`npm`), and JVM +(`maven`) package formats without choosing a package manager or modifying its +configuration files. + +## Requirements + +- Bash and AWS CLI v2 on the runner. No action-specific runtime dependencies or + architecture-specific binaries are installed; Linux amd64 and arm64 are tested. +- AWS credentials configured before this action, for example with + [`aws.configure-credentials`](../aws.configure-credentials/action.yaml). +- `codeartifact:GetAuthorizationToken` on the domain, + `codeartifact:GetRepositoryEndpoint` on the repository, and + `sts:GetServiceBearerToken` with the `sts:AWSServiceName` condition set to + `codeartifact.amazonaws.com`. + +Downloading packages additionally requires `codeartifact:ReadFromRepository`. +Publishing requires the corresponding package write permissions, such as +`codeartifact:PublishPackageVersion` and `codeartifact:PutPackageMetadata`. +Cross-account access also requires the appropriate domain/repository policies. +The action does not grant permissions or create repositories. + +## Usage + +Install AWS CLI v2 first, for example through `mise.setup-tools` and the caller's +`mise.toml`. The following steps assume it is available on `PATH`. + +```yaml +permissions: + contents: read + id-token: write + +jobs: + package: + runs-on: ubuntu-24.04 + steps: + - name: Configure AWS Credentials + uses: tedilabs/github-actions/.github/actions/aws.configure-credentials@main + with: + aws_region: ap-northeast-1 + aws_github_oidc_iam_role: ${{ secrets.AWS_GITHUB_OIDC_IAM_ROLE }} + # Optional: assume a separate CodeArtifact reader or publisher role. + aws_chained_iam_role: ${{ secrets.CODEARTIFACT_IAM_ROLE }} + + - name: Configure CodeArtifact Credentials + id: codeartifact + uses: tedilabs/github-actions/.github/actions/aws.codeartifact.configure-credentials@main + with: + aws_region: ap-northeast-1 + domain: example + domain_owner: "111122223333" + repository: internal + format: pypi +``` + +The `domain_owner` is the account owning the domain, which may differ from the +account of the assumed role. `format` selects an endpoint, not an access level. + +## Inputs and outputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `aws_region` | Yes | | AWS region of the domain. | +| `domain` | Yes | | CodeArtifact domain name. | +| `domain_owner` | Yes | | AWS account ID owning the domain. | +| `repository` | Yes | | CodeArtifact repository name. | +| `format` | Yes | | `pypi`, `npm`, or `maven`. | +| `token_duration_seconds` | No | `0` | `0` or an integer from `900` through `43200`. | + +The default token duration of `0` uses the remaining lifetime of the assumed-role +session. When using IAM user credentials, specify a duration from `900` through +`43200` instead. Long-running jobs must renew AWS credentials and rerun this action +before the credentials expire. See [AWS token authentication](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html). + +After both AWS API calls succeed, subsequent steps in the same job receive: + +- `CODEARTIFACT_AUTH_TOKEN`: masked authorization token. +- `CODEARTIFACT_REPOSITORY_ENDPOINT`: endpoint returned by AWS for `format`. + +The endpoint is also available as `steps..outputs.repository_endpoint`. +There is intentionally no token output. Authenticate separately in each job; +do not put the token in artifacts, caches, or job outputs. Calling this action +again replaces both environment variables for subsequent steps. An unsuccessful +call exports nothing new and fails the step; do not continue after that failure. + +## Package manager configuration + +The caller configures its package manager with these environment variables: + +| Client | How to use the credentials | +| --- | --- | +| uv | Configure a named index, with username `aws` and the token as its password. The index URL is the PyPI endpoint plus `simple/`; the publish URL is the endpoint itself. | +| pip / Twine | pip uses the PyPI endpoint plus `simple/`. Twine uses the endpoint itself with username `aws` and the token as its password. | +| npm / pnpm | Configure the npm endpoint as the registry and reference the token in registry-scoped authentication settings. | +| Yarn | Configure the npm registry and authentication in the project's Yarn configuration. | +| Maven / Gradle | Configure the Maven endpoint with username `aws` and the token as its password. | + +The action preserves the endpoint returned by AWS, including its trailing slash. +It does not append `/simple/` or embed credentials in URLs. Package manager +configuration and cleanup remain under the caller's control. + +## Tests + +Run the regression tests with uv (PyYAML is a test-only dependency): + +```sh +uv run .github/actions/aws.codeartifact.configure-credentials/tests/test_action.py +``` + +The tests execute the actual inline shell step with a mocked AWS CLI. CI also +invokes the composite action and verifies its outputs and subsequent-step +environment on Linux amd64 and arm64. These tests need no AWS credentials and do +not prove access to a live CodeArtifact repository. diff --git a/.github/actions/aws.codeartifact.configure-credentials/action.yaml b/.github/actions/aws.codeartifact.configure-credentials/action.yaml new file mode 100644 index 0000000..2fdc398 --- /dev/null +++ b/.github/actions/aws.codeartifact.configure-credentials/action.yaml @@ -0,0 +1,96 @@ +name: AWS - CodeArtifact Configure Credentials +description: Export a CodeArtifact authorization token and repository endpoint using the job's AWS credentials. + + +inputs: + aws_region: + required: true + description: "(Required) The AWS region of the CodeArtifact domain." + domain: + required: true + description: "(Required) The name of the CodeArtifact domain." + domain_owner: + required: true + description: "(Required) The AWS account ID that owns the CodeArtifact domain." + repository: + required: true + description: "(Required) The name of the CodeArtifact repository." + format: + required: true + description: "(Required) The package format: `pypi`, `npm`, or `maven`." + token_duration_seconds: + required: false + default: "0" + description: "(Optional) The token lifetime in seconds: `0` or `900` through `43200`. Defaults to `0`, which requires assumed-role credentials and uses the remaining role session lifetime." + +outputs: + repository_endpoint: + value: ${{ steps.configure-credentials.outputs.repository_endpoint }} + description: "The repository endpoint for the requested package format, without credentials." + + +runs: + using: composite + + steps: + - name: Configure CodeArtifact Credentials + id: configure-credentials + shell: bash + env: + INPUT_AWS_REGION: ${{ inputs.aws_region }} + INPUT_DOMAIN: ${{ inputs.domain }} + INPUT_DOMAIN_OWNER: ${{ inputs.domain_owner }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_FORMAT: ${{ inputs.format }} + INPUT_TOKEN_DURATION_SECONDS: ${{ inputs.token_duration_seconds }} + AWS_CLI_AUTO_PROMPT: "off" + run: | + set -euo pipefail + + for input in AWS_REGION DOMAIN DOMAIN_OWNER REPOSITORY FORMAT TOKEN_DURATION_SECONDS; do + variable="INPUT_$input" + if [[ -z "${!variable}" ]]; then + echo "::error::Missing required input: $input." + exit 1 + fi + done + + case "$INPUT_FORMAT" in + pypi|npm|maven) ;; + *) echo "::error::format must be pypi, npm, or maven."; exit 1 ;; + esac + + duration="$INPUT_TOKEN_DURATION_SECONDS" + if ! [[ "$duration" =~ ^(0|[1-9][0-9]{0,4})$ ]]; then + echo "::error::token_duration_seconds must be 0 or an integer from 900 through 43200." + exit 1 + fi + if (( duration != 0 && (duration < 900 || duration > 43200) )); then + echo "::error::token_duration_seconds must be 0 or an integer from 900 through 43200." + exit 1 + fi + + aws_args=(--region "$INPUT_AWS_REGION" --domain "$INPUT_DOMAIN" --domain-owner "$INPUT_DOMAIN_OWNER" --output text --no-cli-pager) + token="$(aws codeartifact get-authorization-token "${aws_args[@]}" \ + --duration-seconds "$duration" --query authorizationToken)" + + if [[ -z "$token" || "$token" == None || "$token" == null || "$token" == *$'\n'* || "$token" == *$'\r'* ]]; then + echo "::error::AWS did not return a valid authorization token." + exit 1 + fi + echo "::add-mask::$token" + + endpoint="$(aws codeartifact get-repository-endpoint "${aws_args[@]}" \ + --repository "$INPUT_REPOSITORY" --format "$INPUT_FORMAT" --query repositoryEndpoint)" + + if [[ "$endpoint" != https://* || "$endpoint" == *$'\n'* || "$endpoint" == *$'\r'* ]]; then + echo "::error::AWS did not return a valid repository endpoint." + exit 1 + fi + + # Export only after both API calls succeed. The token is never an action output. + { + echo "CODEARTIFACT_AUTH_TOKEN=$token" + echo "CODEARTIFACT_REPOSITORY_ENDPOINT=$endpoint" + } >> "$GITHUB_ENV" + echo "repository_endpoint=$endpoint" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py b/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py new file mode 100644 index 0000000..39fa4de --- /dev/null +++ b/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Offline AWS CLI fixture shared by regression and composite smoke tests.""" + +import json +import os +import sys + +arguments = sys.argv[1:] +with open(os.environ["MOCK_AWS_CALLS"], "a") as calls: + calls.write(json.dumps(arguments) + "\n") + +operation = arguments[1] +if operation == os.environ.get("MOCK_AWS_FAILURE"): + print("Mock AWS API failure", file=sys.stderr) + sys.exit(1) + +if operation == "get-authorization-token": + print(os.environ.get("MOCK_AWS_TOKEN", "mock-codeartifact-token")) +elif operation == "get-repository-endpoint": + package_format = arguments[arguments.index("--format") + 1] + endpoint = f"https://example-111122223333.d.codeartifact.ap-northeast-1.amazonaws.com/{package_format}/internal/" + print(os.environ.get("MOCK_AWS_ENDPOINT", endpoint)) +else: + sys.exit("Unexpected AWS operation") diff --git a/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py new file mode 100644 index 0000000..1b2b965 --- /dev/null +++ b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py @@ -0,0 +1,106 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["PyYAML==6.0.3"] +# /// +"""Run the actual composite shell step with an offline AWS CLI.""" + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +import yaml + + +ACTION_DIR = Path(__file__).resolve().parents[1] +ACTION = yaml.safe_load((ACTION_DIR / "action.yaml").read_text()) +STEP = ACTION["runs"]["steps"][0] + + +class ConfigureCredentialsTest(unittest.TestCase): + def run_action(self, inputs=None, **mock): + values = { + "aws_region": "ap-northeast-1", + "domain": "example", + "domain_owner": "111122223333", + "repository": "internal", + "format": "pypi", + "token_duration_seconds": ACTION["inputs"]["token_duration_seconds"]["default"], + **(inputs or {}), + } + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + aws = root / "aws" + shutil.copy(ACTION_DIR / "tests/mock-aws.py", aws) + aws.chmod(0o755) + environment = {**os.environ, **mock, "PATH": f"{root}:{os.environ['PATH']}"} + for name, expression in STEP["env"].items(): + if expression.startswith("${{ inputs."): + environment[name] = values[expression.removeprefix("${{ inputs.").removesuffix(" }}")] + else: + environment[name] = expression + files = {name: root / name for name in ("GITHUB_ENV", "GITHUB_OUTPUT", "MOCK_AWS_CALLS")} + for name, path in files.items(): + path.touch() + environment[name] = str(path) + result = subprocess.run( + ["bash", "-c", STEP["run"]], env=environment, text=True, capture_output=True + ) + contents = {name: path.read_text() for name, path in files.items()} + return result, contents + + def test_formats_export_credentials_and_forward_arguments(self): + for package_format in ("pypi", "npm", "maven"): + for duration in ("0", "900", "43200"): + with self.subTest(format=package_format, duration=duration): + result, files = self.run_action({"format": package_format, "token_duration_seconds": duration}) + self.assertEqual(result.returncode, 0, result.stderr) + endpoint = f"https://example-111122223333.d.codeartifact.ap-northeast-1.amazonaws.com/{package_format}/internal/" + self.assertEqual(files["GITHUB_ENV"], f"CODEARTIFACT_AUTH_TOKEN=mock-codeartifact-token\nCODEARTIFACT_REPOSITORY_ENDPOINT={endpoint}\n") + self.assertEqual(files["GITHUB_OUTPUT"], f"repository_endpoint={endpoint}\n") + self.assertEqual(result.stdout, "::add-mask::mock-codeartifact-token\n") + self.assertEqual(result.stderr, "") + calls = [json.loads(line) for line in files["MOCK_AWS_CALLS"].splitlines()] + common = ["--region", "ap-northeast-1", "--domain", "example", "--domain-owner", "111122223333", "--output", "text", "--no-cli-pager"] + self.assertEqual(calls, [ + ["codeartifact", "get-authorization-token", *common, "--duration-seconds", duration, "--query", "authorizationToken"], + ["codeartifact", "get-repository-endpoint", *common, "--repository", "internal", "--format", package_format, "--query", "repositoryEndpoint"], + ]) + + def test_invalid_inputs_fail_before_aws_calls(self): + cases = [{key: ""} for key in ACTION["inputs"]] + cases += [{"format": "python"}] + cases += [{"token_duration_seconds": value} for value in ("-1", "899", "43201", "0900", "1.5", "$(exit 0)", "999999999999999999999")] + for inputs in cases: + with self.subTest(inputs=inputs): + result, files = self.run_action(inputs) + self.assertNotEqual(result.returncode, 0) + self.assertTrue(all(value == "" for value in files.values())) + + def test_api_failures_export_nothing(self): + for operation in ("get-authorization-token", "get-repository-endpoint"): + with self.subTest(operation=operation): + result, files = self.run_action(MOCK_AWS_FAILURE=operation) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(files["GITHUB_ENV"], "") + self.assertEqual(files["GITHUB_OUTPUT"], "") + self.assertIn("Mock AWS API failure", result.stderr) + if operation == "get-repository-endpoint": + self.assertEqual(result.stdout, "::add-mask::mock-codeartifact-token\n") + + def test_invalid_responses_cannot_inject_environment_variables(self): + for key in ("MOCK_AWS_TOKEN", "MOCK_AWS_ENDPOINT"): + for value in ("", "None", "null", "https://example/\nINJECTED=value", "https://example/\rINJECTED=value"): + with self.subTest(key=key, value=value): + result, files = self.run_action(**{key: value}) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(files["GITHUB_ENV"], "") + self.assertEqual(files["GITHUB_OUTPUT"], "") + self.assertNotIn("INJECTED", result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/aws.codeartifact.configure-credentials.test.yaml b/.github/workflows/aws.codeartifact.configure-credentials.test.yaml new file mode 100644 index 0000000..9c96e05 --- /dev/null +++ b/.github/workflows/aws.codeartifact.configure-credentials.test.yaml @@ -0,0 +1,74 @@ +name: Test - CodeArtifact Configure Credentials + + +on: + pull_request: + paths: + - .github/actions/aws.codeartifact.configure-credentials/** + - .github/workflows/aws.codeartifact.configure-credentials.test.yaml + push: + branches: + - main + paths: + - .github/actions/aws.codeartifact.configure-credentials/** + - .github/workflows/aws.codeartifact.configure-credentials.test.yaml + workflow_dispatch: {} + + +permissions: + contents: read + +jobs: + test: + name: Test (${{ matrix.runner }}) + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-24.04 + - ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup Tools + uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main + with: + mise_toml: | + [tools] + uv = "0.13.2" + + - name: Test + run: uv run .github/actions/aws.codeartifact.configure-credentials/tests/test_action.py + + - name: Mock AWS CLI + shell: bash + run: | + mock_bin="$(mktemp -d "$RUNNER_TEMP/codeartifact-test.XXXXXX")" + cp .github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py "$mock_bin/aws" + chmod +x "$mock_bin/aws" + echo "$mock_bin" >> "$GITHUB_PATH" + echo "MOCK_AWS_CALLS=$mock_bin/calls.jsonl" >> "$GITHUB_ENV" + + - name: Smoke Test Composite Action + id: smoke + uses: ./.github/actions/aws.codeartifact.configure-credentials + with: + aws_region: ap-northeast-1 + domain: example + domain_owner: "111122223333" + repository: internal + format: npm + + - name: Check Smoke Test Outputs + shell: bash + env: + REPOSITORY_ENDPOINT: ${{ steps.smoke.outputs.repository_endpoint }} + run: | + test "$CODEARTIFACT_AUTH_TOKEN" = mock-codeartifact-token + test "$CODEARTIFACT_REPOSITORY_ENDPOINT" = https://example-111122223333.d.codeartifact.ap-northeast-1.amazonaws.com/npm/internal/ + test "$REPOSITORY_ENDPOINT" = "$CODEARTIFACT_REPOSITORY_ENDPOINT" From 12b508efdf5f4050d1373e6996a256ce847660e2 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 9 Nov 2026 15:32:23 +0900 Subject: [PATCH 2/4] test(aws.codeartifact.configure-credentials): remove dedicated test workflow --- .../README.md | 8 +- .../tests/mock-aws.py | 2 +- ...deartifact.configure-credentials.test.yaml | 74 ------------------- 3 files changed, 4 insertions(+), 80 deletions(-) delete mode 100644 .github/workflows/aws.codeartifact.configure-credentials.test.yaml diff --git a/.github/actions/aws.codeartifact.configure-credentials/README.md b/.github/actions/aws.codeartifact.configure-credentials/README.md index d5a0cb2..1fb26e6 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/README.md +++ b/.github/actions/aws.codeartifact.configure-credentials/README.md @@ -8,7 +8,7 @@ configuration files. ## Requirements - Bash and AWS CLI v2 on the runner. No action-specific runtime dependencies or - architecture-specific binaries are installed; Linux amd64 and arm64 are tested. + architecture-specific binaries are installed. - AWS credentials configured before this action, for example with [`aws.configure-credentials`](../aws.configure-credentials/action.yaml). - `codeartifact:GetAuthorizationToken` on the domain, @@ -109,7 +109,5 @@ Run the regression tests with uv (PyYAML is a test-only dependency): uv run .github/actions/aws.codeartifact.configure-credentials/tests/test_action.py ``` -The tests execute the actual inline shell step with a mocked AWS CLI. CI also -invokes the composite action and verifies its outputs and subsequent-step -environment on Linux amd64 and arm64. These tests need no AWS credentials and do -not prove access to a live CodeArtifact repository. +The tests execute the actual inline shell step with a mocked AWS CLI. These tests +need no AWS credentials and do not prove access to a live CodeArtifact repository. diff --git a/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py b/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py index 39fa4de..4c86b0c 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py +++ b/.github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Offline AWS CLI fixture shared by regression and composite smoke tests.""" +"""Offline AWS CLI fixture for regression tests.""" import json import os diff --git a/.github/workflows/aws.codeartifact.configure-credentials.test.yaml b/.github/workflows/aws.codeartifact.configure-credentials.test.yaml deleted file mode 100644 index 9c96e05..0000000 --- a/.github/workflows/aws.codeartifact.configure-credentials.test.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Test - CodeArtifact Configure Credentials - - -on: - pull_request: - paths: - - .github/actions/aws.codeartifact.configure-credentials/** - - .github/workflows/aws.codeartifact.configure-credentials.test.yaml - push: - branches: - - main - paths: - - .github/actions/aws.codeartifact.configure-credentials/** - - .github/workflows/aws.codeartifact.configure-credentials.test.yaml - workflow_dispatch: {} - - -permissions: - contents: read - -jobs: - test: - name: Test (${{ matrix.runner }}) - strategy: - fail-fast: false - matrix: - runner: - - ubuntu-24.04 - - ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Setup Tools - uses: tedilabs/github-actions/.github/actions/mise.setup-tools@main - with: - mise_toml: | - [tools] - uv = "0.13.2" - - - name: Test - run: uv run .github/actions/aws.codeartifact.configure-credentials/tests/test_action.py - - - name: Mock AWS CLI - shell: bash - run: | - mock_bin="$(mktemp -d "$RUNNER_TEMP/codeartifact-test.XXXXXX")" - cp .github/actions/aws.codeartifact.configure-credentials/tests/mock-aws.py "$mock_bin/aws" - chmod +x "$mock_bin/aws" - echo "$mock_bin" >> "$GITHUB_PATH" - echo "MOCK_AWS_CALLS=$mock_bin/calls.jsonl" >> "$GITHUB_ENV" - - - name: Smoke Test Composite Action - id: smoke - uses: ./.github/actions/aws.codeartifact.configure-credentials - with: - aws_region: ap-northeast-1 - domain: example - domain_owner: "111122223333" - repository: internal - format: npm - - - name: Check Smoke Test Outputs - shell: bash - env: - REPOSITORY_ENDPOINT: ${{ steps.smoke.outputs.repository_endpoint }} - run: | - test "$CODEARTIFACT_AUTH_TOKEN" = mock-codeartifact-token - test "$CODEARTIFACT_REPOSITORY_ENDPOINT" = https://example-111122223333.d.codeartifact.ap-northeast-1.amazonaws.com/npm/internal/ - test "$REPOSITORY_ENDPOINT" = "$CODEARTIFACT_REPOSITORY_ENDPOINT" From 1d7f34c88d655781589f0a2e7585d3406ccd5ac0 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 9 Nov 2026 15:38:42 +0900 Subject: [PATCH 3/4] fix(aws.codeartifact.configure-credentials): make domain owner optional --- .../README.md | 9 ++++---- .../action.yaml | 11 +++++---- .../tests/test_action.py | 23 +++++++++++++++---- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/actions/aws.codeartifact.configure-credentials/README.md b/.github/actions/aws.codeartifact.configure-credentials/README.md index 1fb26e6..22ffc3d 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/README.md +++ b/.github/actions/aws.codeartifact.configure-credentials/README.md @@ -50,13 +50,14 @@ jobs: with: aws_region: ap-northeast-1 domain: example - domain_owner: "111122223333" + domain_owner: "111122223333" # Optional for same-account access. repository: internal format: pypi ``` -The `domain_owner` is the account owning the domain, which may differ from the -account of the assumed role. `format` selects an endpoint, not an access level. +The `domain_owner` is the account owning the domain. When omitted or empty, AWS +uses the account of the configured credentials. Specify it when the domain belongs +to a different account. `format` selects an endpoint, not an access level. ## Inputs and outputs @@ -64,7 +65,7 @@ account of the assumed role. `format` selects an endpoint, not an access level. | --- | --- | --- | --- | | `aws_region` | Yes | | AWS region of the domain. | | `domain` | Yes | | CodeArtifact domain name. | -| `domain_owner` | Yes | | AWS account ID owning the domain. | +| `domain_owner` | No | Current AWS account | AWS account ID owning the domain. Required for cross-account access. | | `repository` | Yes | | CodeArtifact repository name. | | `format` | Yes | | `pypi`, `npm`, or `maven`. | | `token_duration_seconds` | No | `0` | `0` or an integer from `900` through `43200`. | diff --git a/.github/actions/aws.codeartifact.configure-credentials/action.yaml b/.github/actions/aws.codeartifact.configure-credentials/action.yaml index 2fdc398..07f26e2 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/action.yaml +++ b/.github/actions/aws.codeartifact.configure-credentials/action.yaml @@ -10,8 +10,8 @@ inputs: required: true description: "(Required) The name of the CodeArtifact domain." domain_owner: - required: true - description: "(Required) The AWS account ID that owns the CodeArtifact domain." + required: false + description: "(Optional) The AWS account ID that owns the CodeArtifact domain. Defaults to the account of the configured AWS credentials. Required for cross-account access." repository: required: true description: "(Required) The name of the CodeArtifact repository." @@ -47,7 +47,7 @@ runs: run: | set -euo pipefail - for input in AWS_REGION DOMAIN DOMAIN_OWNER REPOSITORY FORMAT TOKEN_DURATION_SECONDS; do + for input in AWS_REGION DOMAIN REPOSITORY FORMAT TOKEN_DURATION_SECONDS; do variable="INPUT_$input" if [[ -z "${!variable}" ]]; then echo "::error::Missing required input: $input." @@ -70,7 +70,10 @@ runs: exit 1 fi - aws_args=(--region "$INPUT_AWS_REGION" --domain "$INPUT_DOMAIN" --domain-owner "$INPUT_DOMAIN_OWNER" --output text --no-cli-pager) + aws_args=(--region "$INPUT_AWS_REGION" --domain "$INPUT_DOMAIN" --output text --no-cli-pager) + if [[ -n "$INPUT_DOMAIN_OWNER" ]]; then + aws_args+=(--domain-owner "$INPUT_DOMAIN_OWNER") + fi token="$(aws codeartifact get-authorization-token "${aws_args[@]}" \ --duration-seconds "$duration" --query authorizationToken)" diff --git a/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py index 1b2b965..d87f156 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py +++ b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py @@ -25,7 +25,6 @@ def run_action(self, inputs=None, **mock): values = { "aws_region": "ap-northeast-1", "domain": "example", - "domain_owner": "111122223333", "repository": "internal", "format": "pypi", "token_duration_seconds": ACTION["inputs"]["token_duration_seconds"]["default"], @@ -39,7 +38,7 @@ def run_action(self, inputs=None, **mock): environment = {**os.environ, **mock, "PATH": f"{root}:{os.environ['PATH']}"} for name, expression in STEP["env"].items(): if expression.startswith("${{ inputs."): - environment[name] = values[expression.removeprefix("${{ inputs.").removesuffix(" }}")] + environment[name] = values.get(expression.removeprefix("${{ inputs.").removesuffix(" }}"), "") else: environment[name] = expression files = {name: root / name for name in ("GITHUB_ENV", "GITHUB_OUTPUT", "MOCK_AWS_CALLS")} @@ -56,7 +55,7 @@ def test_formats_export_credentials_and_forward_arguments(self): for package_format in ("pypi", "npm", "maven"): for duration in ("0", "900", "43200"): with self.subTest(format=package_format, duration=duration): - result, files = self.run_action({"format": package_format, "token_duration_seconds": duration}) + result, files = self.run_action({"format": package_format, "token_duration_seconds": duration, "domain_owner": "111122223333"}) self.assertEqual(result.returncode, 0, result.stderr) endpoint = f"https://example-111122223333.d.codeartifact.ap-northeast-1.amazonaws.com/{package_format}/internal/" self.assertEqual(files["GITHUB_ENV"], f"CODEARTIFACT_AUTH_TOKEN=mock-codeartifact-token\nCODEARTIFACT_REPOSITORY_ENDPOINT={endpoint}\n") @@ -64,14 +63,28 @@ def test_formats_export_credentials_and_forward_arguments(self): self.assertEqual(result.stdout, "::add-mask::mock-codeartifact-token\n") self.assertEqual(result.stderr, "") calls = [json.loads(line) for line in files["MOCK_AWS_CALLS"].splitlines()] - common = ["--region", "ap-northeast-1", "--domain", "example", "--domain-owner", "111122223333", "--output", "text", "--no-cli-pager"] + common = ["--region", "ap-northeast-1", "--domain", "example", "--output", "text", "--no-cli-pager", "--domain-owner", "111122223333"] self.assertEqual(calls, [ ["codeartifact", "get-authorization-token", *common, "--duration-seconds", duration, "--query", "authorizationToken"], ["codeartifact", "get-repository-endpoint", *common, "--repository", "internal", "--format", package_format, "--query", "repositoryEndpoint"], ]) + def test_domain_owner_can_be_omitted_or_empty(self): + self.assertFalse(ACTION["inputs"]["domain_owner"]["required"]) + for inputs in ({}, {"domain_owner": ""}): + with self.subTest(inputs=inputs): + result, files = self.run_action(inputs) + self.assertEqual(result.returncode, 0, result.stderr) + calls = [json.loads(line) for line in files["MOCK_AWS_CALLS"].splitlines()] + self.assertEqual(len(calls), 2) + for call in calls: + self.assertNotIn("--domain-owner", call) + self.assertNotIn("", call) + self.assertIn("CODEARTIFACT_AUTH_TOKEN=mock-codeartifact-token\n", files["GITHUB_ENV"]) + self.assertTrue(files["GITHUB_OUTPUT"].startswith("repository_endpoint=https://")) + def test_invalid_inputs_fail_before_aws_calls(self): - cases = [{key: ""} for key in ACTION["inputs"]] + cases = [{key: ""} for key in ACTION["inputs"] if key != "domain_owner"] cases += [{"format": "python"}] cases += [{"token_duration_seconds": value} for value in ("-1", "899", "43201", "0900", "1.5", "$(exit 0)", "999999999999999999999")] for inputs in cases: From 4083ab3f2b6da0a217361e7c6e6056b237cf0731 Mon Sep 17 00:00:00 2001 From: Byungjin Park Date: Mon, 9 Nov 2026 15:53:07 +0900 Subject: [PATCH 4/4] feat(aws.codeartifact.configure-credentials): accept all CodeArtifact formats --- .../README.md | 14 ++++++++++---- .../action.yaml | 6 +++--- .../tests/test_action.py | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/actions/aws.codeartifact.configure-credentials/README.md b/.github/actions/aws.codeartifact.configure-credentials/README.md index 22ffc3d..1a82949 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/README.md +++ b/.github/actions/aws.codeartifact.configure-credentials/README.md @@ -1,9 +1,9 @@ # AWS - CodeArtifact Configure Credentials Fetch a CodeArtifact authorization token and a repository endpoint using the job's -existing AWS credentials. Supports Python (`pypi`), Node.js (`npm`), and JVM -(`maven`) package formats without choosing a package manager or modifying its -configuration files. +existing AWS credentials. Supports all CodeArtifact package formats (`cargo`, +`generic`, `maven`, `npm`, `nuget`, `pypi`, `ruby`, and `swift`) without choosing a +package manager or modifying its configuration files. ## Requirements @@ -67,7 +67,7 @@ to a different account. `format` selects an endpoint, not an access level. | `domain` | Yes | | CodeArtifact domain name. | | `domain_owner` | No | Current AWS account | AWS account ID owning the domain. Required for cross-account access. | | `repository` | Yes | | CodeArtifact repository name. | -| `format` | Yes | | `pypi`, `npm`, or `maven`. | +| `format` | Yes | | `cargo`, `generic`, `maven`, `npm`, `nuget`, `pypi`, `ruby`, or `swift`. | | `token_duration_seconds` | No | `0` | `0` or an integer from `900` through `43200`. | The default token duration of `0` uses the remaining lifetime of the assumed-role @@ -98,6 +98,12 @@ The caller configures its package manager with these environment variables: | Yarn | Configure the npm registry and authentication in the project's Yarn configuration. | | Maven / Gradle | Configure the Maven endpoint with username `aws` and the token as its password. | +The `format` values match the [AWS GetRepositoryEndpoint API](https://docs.aws.amazon.com/cli/latest/reference/codeartifact/get-repository-endpoint.html). +All formats use the same token and endpoint retrieval logic. Client configuration +remains format-specific. For `generic`, the usual publishing and downloading path +uses AWS CLI/SDK credentials directly; those API calls do not require the token +exported by this action. See [generic package usage](https://docs.aws.amazon.com/codeartifact/latest/ug/publishing-using-generic-packages.html). + The action preserves the endpoint returned by AWS, including its trailing slash. It does not append `/simple/` or embed credentials in URLs. Package manager configuration and cleanup remain under the caller's control. diff --git a/.github/actions/aws.codeartifact.configure-credentials/action.yaml b/.github/actions/aws.codeartifact.configure-credentials/action.yaml index 07f26e2..251107a 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/action.yaml +++ b/.github/actions/aws.codeartifact.configure-credentials/action.yaml @@ -17,7 +17,7 @@ inputs: description: "(Required) The name of the CodeArtifact repository." format: required: true - description: "(Required) The package format: `pypi`, `npm`, or `maven`." + description: "(Required) The CodeArtifact package format: `cargo`, `generic`, `maven`, `npm`, `nuget`, `pypi`, `ruby`, or `swift`." token_duration_seconds: required: false default: "0" @@ -56,8 +56,8 @@ runs: done case "$INPUT_FORMAT" in - pypi|npm|maven) ;; - *) echo "::error::format must be pypi, npm, or maven."; exit 1 ;; + cargo|generic|maven|npm|nuget|pypi|ruby|swift) ;; + *) echo "::error::format must be cargo, generic, maven, npm, nuget, pypi, ruby, or swift."; exit 1 ;; esac duration="$INPUT_TOKEN_DURATION_SECONDS" diff --git a/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py index d87f156..c52bcdf 100644 --- a/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py +++ b/.github/actions/aws.codeartifact.configure-credentials/tests/test_action.py @@ -52,7 +52,7 @@ def run_action(self, inputs=None, **mock): return result, contents def test_formats_export_credentials_and_forward_arguments(self): - for package_format in ("pypi", "npm", "maven"): + for package_format in ("cargo", "generic", "maven", "npm", "nuget", "pypi", "ruby", "swift"): for duration in ("0", "900", "43200"): with self.subTest(format=package_format, duration=duration): result, files = self.run_action({"format": package_format, "token_duration_seconds": duration, "domain_owner": "111122223333"})