From aa0281a7a891749a2ea23535b41db2edad60a746 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty <79675696+bedatty@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:28:51 -0300 Subject: [PATCH 1/2] feat(go-release): manifest publish for component tags, non-root go.mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The permission_manifest_publish job scoped itself through the root go.mod and accepted only plain release tags, so a monorepo with per-service modules and -vX.Y.Z tags got "manifest publish skipped" on every tag and had to keep a bespoke publisher with its own tag-to-environment derivation. Two additive inputs, both defaulting to today's behaviour: - manifest_go_mod_path (default 'go.mod') is forwarded to the composite, which already accepted go-mod-path — the umbrella just never passed it. It moves only the lib-auth scope gate; discovery stays repo-wide. - manifest_component_tags (default false) allows one optional lowercase component prefix ahead of the version. It is a prefix on the same anchored patterns, never a replacement: NUM, CORE, the anchors and the three-way order are untouched, so v01.2.3 and spi-v1.2.3-hotfix still fall through to the empty folder and publish nothing. Verified against an 18-case tag table; the flag-off column is identical to the current behaviour. Closes #901 --- .github/workflows/go-release.yml | 33 ++++++++++++++++++++++++++++---- docs/go-release.md | 31 +++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index e5482e2b..91dafbad 100644 --- a/.github/workflows/go-release.yml +++ b/.github/workflows/go-release.yml @@ -335,6 +335,14 @@ on: description: 'Name of a GitHub Environment to scope the manifest publish job to. Empty (default) keeps the job unscoped, exactly as before. Set it to put the AWS_INIT_DATA_ROLE_ARN assume-role behind the environment''s protection rules — deployment tag policies (only protected tags may publish) and, optionally, required reviewers.' type: string default: '' + manifest_go_mod_path: + description: 'Path to the go.mod used by the manifest publish scope gate (direct lib-auth dependency check), relative to the repo root. Default is the root go.mod, exactly as before. A monorepo with per-service modules and no root go.mod points this at one module that has the direct dependency — manifest discovery stays repo-wide either way.' + type: string + default: 'go.mod' + manifest_component_tags: + description: 'Accept per-component release tags (-vX.Y.Z, -beta.N, -rc.N) in the manifest publish environment gate, on top of plain vX.Y.Z tags. Default false keeps the gate plain-semver-only, exactly as before. The semver core stays strict either way — only an optional lowercase component prefix is allowed.' + type: boolean + default: false # ----------------- API Dog E2E (api-dog-e2e-tests.yml) ----------------- enable_apidog_e2e: description: 'Run the ApiDog E2E test job on tag push after a successful gitops-update' @@ -877,6 +885,9 @@ jobs: - name: Determine environment folder from tag id: env + env: + # A workflow_call boolean reaches the shell as the string true/false. + MANIFEST_COMPONENT_TAGS: ${{ inputs.manifest_component_tags }} run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/}" @@ -889,17 +900,29 @@ jobs: # otherwise let `v01.2.3` slip past the gate as a production tag. NUM='(0|[1-9][0-9]*)' CORE="v${NUM}\.${NUM}\.${NUM}" - if [[ "$TAG" =~ ^${CORE}-beta\.${NUM}$ ]]; then + # manifest_component_tags widens the gate by ONE optional prefix — a + # monorepo cutting `-vX.Y.Z` per rail. It is a prefix on the + # same anchored patterns, never a replacement: the semver core, the + # anchors and the three-way order below are untouched, so `v01.2.3` + # and `spi-v1.2.3-hotfix` keep falling through to the empty folder. + PREFIX='' + if [[ "${MANIFEST_COMPONENT_TAGS}" == 'true' ]]; then + PREFIX='([a-z][a-z0-9-]*-)?' + SHAPES='vX.Y.Z, vX.Y.Z-beta.N, vX.Y.Z-rc.N, optionally prefixed with -' + else + SHAPES='vX.Y.Z, vX.Y.Z-beta.N, vX.Y.Z-rc.N' + fi + if [[ "$TAG" =~ ^${PREFIX}${CORE}-beta\.${NUM}$ ]]; then FOLDER="development" - elif [[ "$TAG" =~ ^${CORE}-rc\.${NUM}$ ]]; then + elif [[ "$TAG" =~ ^${PREFIX}${CORE}-rc\.${NUM}$ ]]; then FOLDER="staging" - elif [[ "$TAG" =~ ^${CORE}$ ]]; then + elif [[ "$TAG" =~ ^${PREFIX}${CORE}$ ]]; then FOLDER="production" else FOLDER="" fi echo "folder=${FOLDER}" >> "$GITHUB_OUTPUT" - [[ -n "$FOLDER" ]] && echo "📁 Environment: ${FOLDER}" || echo "::warning::Tag '${TAG}' is not a release semver tag (vX.Y.Z, vX.Y.Z-beta.N, vX.Y.Z-rc.N) — manifest publish skipped." + [[ -n "$FOLDER" ]] && echo "📁 Environment: ${FOLDER}" || echo "::warning::Tag '${TAG}' is not a release semver tag (${SHAPES}) — manifest publish skipped." # ----------------- AWS credentials (guarded) ----------------- # Skip on a dry-run (the composite previews with no AWS call) and when the @@ -920,6 +943,8 @@ jobs: with: environment: ${{ steps.env.outputs.folder }} github-token: ${{ github.token }} + # Only moves the lib-auth scope gate; manifest discovery stays repo-wide. + go-mod-path: ${{ inputs.manifest_go_mod_path }} dry-run: ${{ inputs.dry_run }} # ----------------- Extra Builds (tag push, opt-in) ----------------- diff --git a/docs/go-release.md b/docs/go-release.md index 7414269a..809bcbb4 100644 --- a/docs/go-release.md +++ b/docs/go-release.md @@ -94,6 +94,8 @@ A third layout needs `release_single_app: true`: **one semantic-release tag for | `s3_upload_mirrors` | JSON object mapping an `s3_uploads` bucket to a second bucket that receives every file uploaded to it under the same key (see [Mirror bucket](#mirror-bucket)) | string | `{"lerian-migration-files":{"bucket":"lerian-development-migrations","region":"us-east-2"}}` | | `run_manifest_publish` | Publish this service's own `permissions.yaml` to the shared Access-Manager RI catalog in S3 on tag push (see [Permission manifest publish](#permission-manifest-publish-ri)) | boolean | `true` | | `manifest_publish_environment` | GitHub Environment to scope the manifest publish job to, putting the `AWS_INIT_DATA_ROLE_ARN` assume-role behind that environment's protection rules. Empty keeps the job unscoped | string | `''` | +| `manifest_go_mod_path` | Path to the `go.mod` the manifest publish scope gate reads for the direct lib-auth dependency, relative to the repo root. Point it at a service module in a monorepo with no root `go.mod` — discovery stays repo-wide | string | `'go.mod'` | +| `manifest_component_tags` | Also accept per-component release tags (`-vX.Y.Z`, `-beta.N`, `-rc.N`) in the environment gate. The semver core stays strict either way | boolean | `false` | | `enable_apidog_e2e` | Run the ApiDog E2E test job on tag push after a successful gitops-update | boolean | `false` | | `apidog_runner_type` | Runner for the ApiDog E2E test job (needs reach to the deployed environment) | string | `eveo-lxc-runners` | | `apidog_auto_detect_environment` | Auto-detect the ApiDog environment from the tag (beta → dev, rc → stg); when `false`, uses `APIDOG_ENVIRONMENT_ID` | boolean | `true` | @@ -230,7 +232,34 @@ The catalog is **per-service and env-scoped**: each service writes exactly one o s3://lerian-casdoor-init-data/{environment}/permissions/{service}.yaml ``` -The `{environment}` folder is derived from the pushed tag, which must be a **strict release semver tag**: `vX.Y.Z-beta.N` → `development`, `vX.Y.Z-rc.N` → `staging`, `vX.Y.Z` → `production`. That is exactly what semantic-release produces; any other tag shape publishes **nothing** (the job logs a warning and skips) so a stray tag merely containing `beta`/`rc` can never write to the shared catalog. There is **no aggregation** on write — the tenant-manager aggregates at read time by listing the `{environment}/permissions/` prefix, and each release overwrites only its own file. +The `{environment}` folder is derived from the pushed tag, which must be a **strict release semver tag**: `vX.Y.Z-beta.N` → `development`, `vX.Y.Z-rc.N` → `staging`, `vX.Y.Z` → `production`. That is exactly what semantic-release produces; any other tag shape publishes **nothing** (the job logs a warning and skips) so a stray tag merely containing `beta`/`rc` can never write to the shared catalog. + +### Per-component tags and a non-root `go.mod` + +A monorepo that cuts one version line per service — tags like `spi-v1.2.3`, `slc-edge-v1.2.3-beta.4` — matches none of those shapes and publishes nothing. Set `manifest_component_tags: true` to allow **one optional lowercase component prefix** before the version: + +| Tag | `manifest_component_tags: false` | `manifest_component_tags: true` | +|---|---|---| +| `v1.2.3` | `production` | `production` | +| `spi-v1.2.3` | skipped | `production` | +| `slc-edge-v1.2.3-beta.4` | skipped | `development` | +| `cockpit-v2.0.0-rc.3` | skipped | `staging` | +| `v01.2.3` | skipped | skipped | +| `spi-v1.2.3-hotfix` | skipped | skipped | + +The prefix is the **only** thing that widens: the semver core keeps rejecting leading zeroes and arbitrary suffixes, and the prefix itself must start with a lowercase letter and hold only lowercase letters, digits and hyphens. A tag off-shape still publishes nothing. Leave the input at its default and the gate behaves exactly as it always has. + +`manifest_go_mod_path` moves **only** the lib-auth scope gate. A repo with per-service modules and no root `go.mod` points it at one module carrying the direct dependency; manifest discovery stays repo-wide, so the repo still publishes one object per `service:` value found anywhere in the tree: + +```yaml + uses: LerianStudio/github-actions-shared-workflows/.github/workflows/go-release.yml@tier-1 + with: + run_manifest_publish: true + manifest_go_mod_path: services/spi/go.mod + manifest_component_tags: true + secrets: + AWS_INIT_DATA_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }} +``` There is **no aggregation** on write — the tenant-manager aggregates at read time by listing the `{environment}/permissions/` prefix, and each release overwrites only its own file. The job assumes the **`AWS_INIT_DATA_ROLE_ARN`** secret (scoped to the `lerian-casdoor-init-data` bucket — the same bucket the Casdoor `init_data.json` uses) via OIDC in region `sa-east-1`. Prefer mapping it **explicitly** rather than relying on `secrets: inherit`, so the release only ever receives the secrets it needs: From 7cc75c40b66c6daf59ee3a7486e610e1e4ef3f74 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty <79675696+bedatty@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:24:20 -0300 Subject: [PATCH 2/2] docs(go-release): fix the section the component-tag docs broke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new subsection landed inside the paragraph that ends the environment-folder rule and ahead of the rest of the RI section, so "There is no aggregation on write" was severed from its paragraph and glued to the caller snippet's closing fence — the example rendered open-ended — and the AWS role, opt-out and manifest_publish_environment paragraphs, which describe the section as a whole, ended up under a heading about component tags. Rejoin the sentence to its paragraph and move the subsection to the end of the section. The docs diff is now purely additive. --- docs/go-release.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/go-release.md b/docs/go-release.md index 809bcbb4..27695aab 100644 --- a/docs/go-release.md +++ b/docs/go-release.md @@ -232,7 +232,18 @@ The catalog is **per-service and env-scoped**: each service writes exactly one o s3://lerian-casdoor-init-data/{environment}/permissions/{service}.yaml ``` -The `{environment}` folder is derived from the pushed tag, which must be a **strict release semver tag**: `vX.Y.Z-beta.N` → `development`, `vX.Y.Z-rc.N` → `staging`, `vX.Y.Z` → `production`. That is exactly what semantic-release produces; any other tag shape publishes **nothing** (the job logs a warning and skips) so a stray tag merely containing `beta`/`rc` can never write to the shared catalog. +The `{environment}` folder is derived from the pushed tag, which must be a **strict release semver tag**: `vX.Y.Z-beta.N` → `development`, `vX.Y.Z-rc.N` → `staging`, `vX.Y.Z` → `production`. That is exactly what semantic-release produces; any other tag shape publishes **nothing** (the job logs a warning and skips) so a stray tag merely containing `beta`/`rc` can never write to the shared catalog. There is **no aggregation** on write — the tenant-manager aggregates at read time by listing the `{environment}/permissions/` prefix, and each release overwrites only its own file. + +The job assumes the **`AWS_INIT_DATA_ROLE_ARN`** secret (scoped to the `lerian-casdoor-init-data` bucket — the same bucket the Casdoor `init_data.json` uses) via OIDC in region `sa-east-1`. Prefer mapping it **explicitly** rather than relying on `secrets: inherit`, so the release only ever receives the secrets it needs: + +```yaml + secrets: + AWS_INIT_DATA_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }} +``` + +Set `run_manifest_publish: false` to opt out. + +For an extra gate on **who** may publish, set `manifest_publish_environment` to the name of a GitHub Environment configured in the caller repo. The publish job then runs under that environment, so its protection rules apply to the assume-role — a **deployment tag policy** (e.g. `v*.*.*`) restricts publishing to the tags you protect, and required reviewers can hold a production publish for approval. Left empty (the default) the job stays unscoped, as before. ### Per-component tags and a non-root `go.mod` @@ -259,19 +270,8 @@ The prefix is the **only** thing that widens: the semver core keeps rejecting le manifest_component_tags: true secrets: AWS_INIT_DATA_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }} -``` There is **no aggregation** on write — the tenant-manager aggregates at read time by listing the `{environment}/permissions/` prefix, and each release overwrites only its own file. - -The job assumes the **`AWS_INIT_DATA_ROLE_ARN`** secret (scoped to the `lerian-casdoor-init-data` bucket — the same bucket the Casdoor `init_data.json` uses) via OIDC in region `sa-east-1`. Prefer mapping it **explicitly** rather than relying on `secrets: inherit`, so the release only ever receives the secrets it needs: - -```yaml - secrets: - AWS_INIT_DATA_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }} ``` -Set `run_manifest_publish: false` to opt out. - -For an extra gate on **who** may publish, set `manifest_publish_environment` to the name of a GitHub Environment configured in the caller repo. The publish job then runs under that environment, so its protection rules apply to the assume-role — a **deployment tag policy** (e.g. `v*.*.*`) restricts publishing to the tags you protect, and required reviewers can hold a production publish for approval. Left empty (the default) the job stays unscoped, as before. - ## ApiDog E2E tests Set `enable_apidog_e2e: true` to run [api-dog-e2e-tests](./api-dog-e2e-tests.md) on tag push after a successful `update_gitops`. The job is skipped on branch pushes and when the gitops update did not succeed.