diff --git a/.github/workflows/release-extension.yml b/.github/workflows/release-extension.yml index 880542e97..a6c6edf8c 100644 --- a/.github/workflows/release-extension.yml +++ b/.github/workflows/release-extension.yml @@ -10,36 +10,13 @@ name: Release Lambda Extension # patch on top of upstream v119 -> Tero-Datadog-Extension-119:2 # # Tag "v119" to release, "v119.2" to release a patch and assert it lands on 2. +# A tag push is the only automated way in. upstream-release-watch.yml pushes the +# tag; publishing lives here and nowhere else. on: push: tags: - "v[0-9]+" - "v[0-9]+.[0-9]+" - # Called by upstream-release-watch.yml. A tag pushed with GITHUB_TOKEN does not - # trigger another workflow, so the watch job invokes this one directly instead - # of relying on a tag push. github.event_name is the caller's event, so a - # scheduled watch run does NOT get the -dev suffix — it is a real release. - workflow_call: - inputs: - version: - description: "Upstream version to release, e.g. 119 or 119.2 (patch optional)" - required: true - type: string - regions: - description: "Comma-separated AWS regions. Empty publishes to DEFAULT_REGIONS." - required: false - default: "" - type: string - architectures: - description: "Architectures to build and publish" - required: false - default: "amd64,arm64" - type: string - dry_run: - description: "Dry run - build but don't publish to AWS" - required: false - default: false - type: boolean workflow_dispatch: inputs: version: @@ -65,6 +42,13 @@ on: required: false default: false type: boolean + # Defaults true here so a manual run cannot overwrite a real release by + # accident. Untick it to publish prod layer names by hand. + dev: + description: "Append -dev to the layer name" + required: false + default: true + type: boolean permissions: id-token: write @@ -262,8 +246,12 @@ jobs: COMPATIBLE_ARCH="arm64" fi - # Add -dev suffix for workflow_dispatch (manual) runs - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Driven by an explicit input, not github.event_name: inside a called + # workflow event_name is the CALLER's event, so keying off it made a + # manual run of upstream-release-watch publish -dev names while + # reporting a release. Tag pushes leave the input empty, so they are + # never -dev. + if [ "${{ inputs.dev }}" = "true" ]; then DEV_SUFFIX="-dev" else DEV_SUFFIX="" diff --git a/.github/workflows/upstream-release-watch.yml b/.github/workflows/upstream-release-watch.yml index c61faae40..9dcd0200b 100644 --- a/.github/workflows/upstream-release-watch.yml +++ b/.github/workflows/upstream-release-watch.yml @@ -216,13 +216,53 @@ jobs: "git checkout -b chore/upstream-v${UPSTREAM_VERSION} && git merge v${UPSTREAM_VERSION}" \ '```' )" - release: - name: Release v${{ needs.check.outputs.upstream_version }} + # Tagging IS the release: pushing v triggers release-extension.yml, which + # owns publishing. There is deliberately no second path from here. + # + # The push needs RELEASE_TAG_TOKEN (a PAT or GitHub App token with + # contents:write). A tag pushed with the default GITHUB_TOKEN does not trigger + # another workflow, so with GITHUB_TOKEN the tag would land and nothing would + # publish. + tag: + name: Tag v${{ needs.check.outputs.upstream_version }} + runs-on: ubuntu-22.04 needs: check if: | needs.check.outputs.needs_release == 'true' && (github.event_name == 'schedule' || inputs.release) - uses: ./.github/workflows/release-extension.yml - with: - version: ${{ needs.check.outputs.upstream_version }} - secrets: inherit + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + # Checked out with the PAT so the tag push below is attributed to it + # and triggers the release workflow. + token: ${{ secrets.RELEASE_TAG_TOKEN }} + + - name: Tag the release + env: + GH_TOKEN: ${{ secrets.RELEASE_TAG_TOKEN }} + UPSTREAM_VERSION: ${{ needs.check.outputs.upstream_version }} + run: | + set -euo pipefail + TAG="v${UPSTREAM_VERSION}" + + # The tag already existing means a previous release was tagged but did + # not publish, so re-tagging cannot retry it. Fail loudly rather than + # leave the fork silently behind. + if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "::error::$TAG is already tagged but Tero-Datadog-Extension-${UPSTREAM_VERSION} is not published." + echo "::error::Re-run Release Lambda Extension for $UPSTREAM_VERSION with dev unticked, then check why the tagged run failed." + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "$TAG" + git push origin "$TAG" + + gh release create "$TAG" \ + --title "$TAG" \ + --notes "Tracks [DataDog ${TAG}](https://github.com/DataDog/datadog-lambda-extension/releases/tag/${TAG}). Publishes \`Tero-Datadog-Extension-${UPSTREAM_VERSION}\` and \`Tero-Datadog-Extension-${UPSTREAM_VERSION}-ARM\`." + + echo "::notice::Pushed $TAG. Release Lambda Extension takes over from here." diff --git a/Releasing.md b/Releasing.md index beea2108d..c4b651e5b 100644 --- a/Releasing.md +++ b/Releasing.md @@ -106,6 +106,19 @@ Replace: - **Name:** `AWS_ROLE_ARN` - **Value:** Your role ARN +### Step 4: Add a tag-push token + +Upstream Release Watch pushes the release tag, and a tag pushed with the default +`GITHUB_TOKEN` does not trigger another workflow. Without this secret the watch +tags and nothing publishes. + +1. Create a fine-grained PAT (or GitHub App token) with **Contents: write** on + this repository + +2. Add it as a secret: + - **Name:** `RELEASE_TAG_TOKEN` + - **Value:** The token + ## Versioning Releases track DataDog's upstream releases. Upstream publishes `v` tags, so a @@ -133,17 +146,24 @@ be triggered by hand. It compares DataDog's latest release with two things: the version `main` contains (recorded in `.upstream-version`) and whether `Tero-Datadog-Extension-` is published. -| State | What the watch does | -| ----------------------------------------- | ------------------------------------ | -| `main` is behind upstream | Opens a PR merging upstream `v` | -| `main` is level, `v` not published | Publishes `v` to all regions | -| Both level | Nothing | +| State | What the watch does | +| ------------------------------------- | ---------------------------------- | +| `main` is behind upstream | Opens a PR merging upstream `v` | +| `main` is level, `v` not published | Pushes tag `v` | +| Both level | Nothing | + +The watch never publishes. It pushes the tag, and the tag triggers **Release +Lambda Extension** — publishing lives in one place, on one trigger. A release therefore only happens after a human merged the upstream PR and CI was green. When the merge conflicts the watch opens an issue instead of a PR, because resolving an upstream merge needs judgement — stale lock entries and API changes that only clippy catches. +If the tag already exists but the layer is still unpublished, the watch fails +loudly rather than re-tagging: a tag can only trigger a release once, so that +state means a tagged release failed and needs a look. + To check parity without publishing, run it by hand with `release` unticked. `.upstream-version` is the record of what `main` contains. The merge PR bumps it; @@ -190,8 +210,10 @@ publishing to a region the account has not enabled fails the job. Add them to 4. After completion, find the Layer ARNs in the workflow summary -Manual runs append `-dev` to the layer name, so they never overwrite a real -release. +Manual runs tick `dev` by default, which appends `-dev` to the layer name so they +cannot overwrite a real release. Untick it to publish prod names by hand. + +Tag pushes and watch-driven releases always publish prod names. ## Layer Naming Convention