From fe4b6efb6d1b2c73e90db4aec191cc8e1959e071 Mon Sep 17 00:00:00 2001 From: Paul Falgout Date: Sat, 2 May 2026 00:24:33 +0900 Subject: [PATCH 1/3] ci(linear): wire Linear release pipeline to CI Add Linear release stage updates to the tag-build artifact pipeline and the deploy pipeline. Tag artifact upload syncs the release and moves it to Started; qa/sandbox/prod deploys advance through QA, Sandbox, and Released stages, with complete called after a successful prod deploy. Skips dev deploys and prod:demonstration. Pinned CLI binary v0.7.0 with sha256 verification, scoped via the linear-secrets CircleCI context. Co-Authored-By: Claude Opus 4.7 --- .circleci/config.yml | 29 +++++++++++++++++++++++++++- .circleci/deploy.yml | 45 ++++++++++++++++++++++++++++++++++++++++++++ docs/deploy.md | 11 +++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 41a06581..e6673ebe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,6 +70,19 @@ commands: role_arn: ${AWS_ROLE_ARN} role_session_name: app-frontend-${CIRCLE_BUILD_NUM} + download-linear-release: + steps: + - run: + name: Download Linear Release CLI + command: | + set -euo pipefail + linear_release_version="v0.7.0" + linear_release_sha256="c82e10e79ac54bfa5efff69124add2aa793d91b0d5e32c1ed56ab856eb2a7e79" + linear_release_url="https://github.com/linear/linear-release/releases/download/${linear_release_version}/linear-release-linux-x64" + curl -fsSL "$linear_release_url" -o /tmp/linear-release + echo "${linear_release_sha256} /tmp/linear-release" | sha256sum -c - + chmod +x /tmp/linear-release + # ------------------------------------------------------------ # EXECUTOR – Node 22 LTS image with Chrome 137, FF 139, Edge 137 # ------------------------------------------------------------ @@ -274,6 +287,18 @@ jobs: command: | set -euo pipefail circleci run release update "release-<< pipeline.git.tag >>" --status=FAILED + - download-linear-release + - run: + name: Sync Linear release + when: on_success + command: | + set -euo pipefail + git fetch --force --tags origin + /tmp/linear-release sync \ + --release-version="<< pipeline.git.tag >>" + /tmp/linear-release update \ + --release-version="<< pipeline.git.tag >>" \ + --stage="Started" # ------------------------------------------------------------ # WORKFLOWS @@ -428,6 +453,8 @@ workflows: - publish-build-artifact: <<: *tag-filter name: publish release artifact - context: aws-dev + context: + - aws-dev + - linear-secrets requires: - build diff --git a/.circleci/deploy.yml b/.circleci/deploy.yml index 492bd1ff..fdaef61c 100644 --- a/.circleci/deploy.yml +++ b/.circleci/deploy.yml @@ -58,6 +58,19 @@ commands: role_arn: ${AWS_ROLE_ARN} role_session_name: app-frontend-${CIRCLE_BUILD_NUM} + download-linear-release: + steps: + - run: + name: Download Linear Release CLI + command: | + set -euo pipefail + linear_release_version="v0.7.0" + linear_release_sha256="c82e10e79ac54bfa5efff69124add2aa793d91b0d5e32c1ed56ab856eb2a7e79" + linear_release_url="https://github.com/linear/linear-release/releases/download/${linear_release_version}/linear-release-linux-x64" + curl -fsSL "$linear_release_url" -o /tmp/linear-release + echo "${linear_release_sha256} /tmp/linear-release" | sha256sum -c - + chmod +x /tmp/linear-release + jobs: deploy-from-artifact: docker: @@ -205,6 +218,36 @@ jobs: --status-file="$DEPLOY_MARKER_STATUS_FILE" \ --target-environment="$DEPLOY_TARGET_ENV" \ --target-version="$DEPLOY_TARGET_VERSION" + - download-linear-release + - run: + name: Update Linear release stage + when: on_success + command: | + set -euo pipefail + + case "$DEPLOY_STAGE" in + qa) linear_stage="QA" ;; + sandbox) linear_stage="Sandbox" ;; + prod) + if [ "$DEPLOY_ORGANIZATION" = "demonstration" ]; then + echo "Skipping Linear release update for prod:demonstration" + exit 0 + fi + linear_stage="Released" + ;; + *) exit 0 ;; + esac + + git fetch --force --tags origin + + /tmp/linear-release update \ + --release-version="$DEPLOY_TARGET_VERSION" \ + --stage="$linear_stage" + + if [ "$DEPLOY_STAGE" = "prod" ]; then + /tmp/linear-release complete \ + --release-version="$DEPLOY_TARGET_VERSION" + fi - run: name: Notify QA2 E2E repo when: on_success @@ -265,6 +308,7 @@ workflows: context: - aws-dev - slack-secrets + - linear-secrets deploy-prod: when: @@ -276,3 +320,4 @@ workflows: context: - aws-prod - slack-secrets + - linear-secrets diff --git a/docs/deploy.md b/docs/deploy.md index 220d1f6d..14311c90 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -140,6 +140,12 @@ npm run deploy -- --stage= [--organization=] - stage-wide deploys continue through every resolved environment and fail the job at the end if any targets fail - if a stage-wide deploy partially succeeds, concrete environment markers reflect the per-environment outcomes while the wildcard marker reflects the overall deploy result 6. For QA deploys that include `qa2` (`qa:qa2` and `qa:*`), posts `qa2_deploy_succeeded` to `RoundingWell/app-tests` +7. Updates the Linear release stage by running the pinned `linear/linear-release` CLI: + - `qa` deploys → `update --stage=QA` + - `sandbox` deploys → `update --stage=Sandbox` + - `prod` deploys → `update --stage=Released`, then `complete` + - `prod:demonstration` deploys are skipped (demo org, not a real release event) + - `dev` deploys are skipped Supported deploy environments: - `dev:` @@ -167,6 +173,11 @@ Additional CircleCI secrets for the QA2 E2E dispatch step: - `GH_APP_PRIVATE_KEY` - `GH_APP_INSTALLATION_ID` +CircleCI context for the Linear release steps: +- `linear-secrets` context, providing `LINEAR_ACCESS_KEY` (Linear release pipeline access key) +- attached to the `release-artifact` workflow in [`.circleci/config.yml`](../.circleci/config.yml) (sync + `Started` stage on tag build) and to both deploy workflows in [`.circleci/deploy.yml`](../.circleci/deploy.yml) (per-stage `update` and final `complete`) +- the Linear release pipeline is configured as **scheduled**; stages used: built-in `Started`, custom `QA` (frozen) and `Sandbox`, and built-in terminal `Released`. CI also calls `complete` after a successful prod deploy. + For QA deploys that include `qa2`, [`.circleci/deploy.yml`](../.circleci/deploy.yml) resolves the release SHA, passes the release tag, SHA, and a CircleCI run URL to [`scripts/dispatch-qa2-e2e.js`](../scripts/dispatch-qa2-e2e.js), and that script uses the GitHub App credentials above plus the `app-tests` installation id to mint a short-lived installation token before posting `repository_dispatch` with this payload: ```json From 1634f93a19735e0f5c274837650502a98ad42b62 Mon Sep 17 00:00:00 2001 From: Paul Falgout Date: Sat, 2 May 2026 00:41:51 +0900 Subject: [PATCH 2/3] ci(linear): harden release integration per PR review - Make Linear sync/update steps failure-tolerant so a Linear API or download failure cannot fail the deploy/artifact job (Devin, cubic). - Move the deploy-side Linear update step after slack/notify so the deploy result reflects the actual deploy, not Linear (Devin, cubic). - Restrict prod release completion to wildcard prod:* only; org-scoped prod deploys (prod:demonstration, prod:apple, etc.) are skipped so they cannot prematurely complete a release (cubic). - Extract the pinned CLI download to scripts/download-linear-release.sh; config.yml and deploy.yml both call it, removing the duplicated version + sha256 (Copilot). - Inline the download into the gated deploy step so dev deploys skip the network call entirely (Copilot). - Split deploy-dev workflow into deploy-dev and deploy-qa so the linear-secrets context is only attached when actually used (Copilot). Co-Authored-By: Claude Opus 4.7 --- .circleci/config.yml | 31 ++++------- .circleci/deploy.yml | 89 +++++++++++++++--------------- docs/deploy.md | 13 +++-- scripts/download-linear-release.sh | 17 ++++++ 4 files changed, 79 insertions(+), 71 deletions(-) create mode 100755 scripts/download-linear-release.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index e6673ebe..8a8be0b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,19 +70,6 @@ commands: role_arn: ${AWS_ROLE_ARN} role_session_name: app-frontend-${CIRCLE_BUILD_NUM} - download-linear-release: - steps: - - run: - name: Download Linear Release CLI - command: | - set -euo pipefail - linear_release_version="v0.7.0" - linear_release_sha256="c82e10e79ac54bfa5efff69124add2aa793d91b0d5e32c1ed56ab856eb2a7e79" - linear_release_url="https://github.com/linear/linear-release/releases/download/${linear_release_version}/linear-release-linux-x64" - curl -fsSL "$linear_release_url" -o /tmp/linear-release - echo "${linear_release_sha256} /tmp/linear-release" | sha256sum -c - - chmod +x /tmp/linear-release - # ------------------------------------------------------------ # EXECUTOR – Node 22 LTS image with Chrome 137, FF 139, Edge 137 # ------------------------------------------------------------ @@ -287,18 +274,20 @@ jobs: command: | set -euo pipefail circleci run release update "release-<< pipeline.git.tag >>" --status=FAILED - - download-linear-release - run: name: Sync Linear release when: on_success command: | - set -euo pipefail - git fetch --force --tags origin - /tmp/linear-release sync \ - --release-version="<< pipeline.git.tag >>" - /tmp/linear-release update \ - --release-version="<< pipeline.git.tag >>" \ - --stage="Started" + set -uo pipefail + { + ./scripts/download-linear-release.sh + git fetch --force --tags origin + /tmp/linear-release sync \ + --release-version="<< pipeline.git.tag >>" + /tmp/linear-release update \ + --release-version="<< pipeline.git.tag >>" \ + --stage="Started" + } || echo "Linear release sync failed; artifact published. Sync manually if needed." # ------------------------------------------------------------ # WORKFLOWS diff --git a/.circleci/deploy.yml b/.circleci/deploy.yml index fdaef61c..771a3d4d 100644 --- a/.circleci/deploy.yml +++ b/.circleci/deploy.yml @@ -58,19 +58,6 @@ commands: role_arn: ${AWS_ROLE_ARN} role_session_name: app-frontend-${CIRCLE_BUILD_NUM} - download-linear-release: - steps: - - run: - name: Download Linear Release CLI - command: | - set -euo pipefail - linear_release_version="v0.7.0" - linear_release_sha256="c82e10e79ac54bfa5efff69124add2aa793d91b0d5e32c1ed56ab856eb2a7e79" - linear_release_url="https://github.com/linear/linear-release/releases/download/${linear_release_version}/linear-release-linux-x64" - curl -fsSL "$linear_release_url" -o /tmp/linear-release - echo "${linear_release_sha256} /tmp/linear-release" | sha256sum -c - - chmod +x /tmp/linear-release - jobs: deploy-from-artifact: docker: @@ -218,36 +205,6 @@ jobs: --status-file="$DEPLOY_MARKER_STATUS_FILE" \ --target-environment="$DEPLOY_TARGET_ENV" \ --target-version="$DEPLOY_TARGET_VERSION" - - download-linear-release - - run: - name: Update Linear release stage - when: on_success - command: | - set -euo pipefail - - case "$DEPLOY_STAGE" in - qa) linear_stage="QA" ;; - sandbox) linear_stage="Sandbox" ;; - prod) - if [ "$DEPLOY_ORGANIZATION" = "demonstration" ]; then - echo "Skipping Linear release update for prod:demonstration" - exit 0 - fi - linear_stage="Released" - ;; - *) exit 0 ;; - esac - - git fetch --force --tags origin - - /tmp/linear-release update \ - --release-version="$DEPLOY_TARGET_VERSION" \ - --stage="$linear_stage" - - if [ "$DEPLOY_STAGE" = "prod" ]; then - /tmp/linear-release complete \ - --release-version="$DEPLOY_TARGET_VERSION" - fi - run: name: Notify QA2 E2E repo when: on_success @@ -296,12 +253,56 @@ jobs: } ] } + - run: + name: Update Linear release stage + when: on_success + command: | + set -uo pipefail + + case "$DEPLOY_STAGE" in + qa) linear_stage="QA" ;; + sandbox) linear_stage="Sandbox" ;; + prod) + # Only the wildcard prod:* deploy represents a real release + # event. Org-scoped prod deploys (e.g. prod:demonstration, + # prod:apple) must not advance or complete the Linear release. + if [ -n "$DEPLOY_ORGANIZATION" ]; then + echo "Skipping Linear release update for prod:$DEPLOY_ORGANIZATION" + exit 0 + fi + linear_stage="Released" + ;; + *) exit 0 ;; + esac + + { + ./scripts/download-linear-release.sh + git fetch --force --tags origin + /tmp/linear-release update \ + --release-version="$DEPLOY_TARGET_VERSION" \ + --stage="$linear_stage" + if [ "$DEPLOY_STAGE" = "prod" ]; then + /tmp/linear-release complete \ + --release-version="$DEPLOY_TARGET_VERSION" + fi + } || echo "Linear release update failed; deploy succeeded. Sync manually if needed." workflows: deploy-dev: when: matches: - pattern: '^(dev|qa):[a-z0-9*-]+$' + pattern: '^dev:[a-z0-9*-]+$' + value: << pipeline.deploy.environment_name >> + jobs: + - deploy-from-artifact: + context: + - aws-dev + - slack-secrets + + deploy-qa: + when: + matches: + pattern: '^qa:[a-z0-9*-]+$' value: << pipeline.deploy.environment_name >> jobs: - deploy-from-artifact: diff --git a/docs/deploy.md b/docs/deploy.md index 14311c90..d58c6e20 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -140,12 +140,13 @@ npm run deploy -- --stage= [--organization=] - stage-wide deploys continue through every resolved environment and fail the job at the end if any targets fail - if a stage-wide deploy partially succeeds, concrete environment markers reflect the per-environment outcomes while the wildcard marker reflects the overall deploy result 6. For QA deploys that include `qa2` (`qa:qa2` and `qa:*`), posts `qa2_deploy_succeeded` to `RoundingWell/app-tests` -7. Updates the Linear release stage by running the pinned `linear/linear-release` CLI: - - `qa` deploys → `update --stage=QA` - - `sandbox` deploys → `update --stage=Sandbox` - - `prod` deploys → `update --stage=Released`, then `complete` - - `prod:demonstration` deploys are skipped (demo org, not a real release event) +7. Updates the Linear release stage by running the pinned `linear/linear-release` CLI (downloaded by [`scripts/download-linear-release.sh`](../scripts/download-linear-release.sh)): + - `qa:*` and specific `qa:` deploys → `update --stage=QA` + - `sandbox:*` and specific `sandbox:` deploys → `update --stage=Sandbox` + - `prod:*` (wildcard only) → `update --stage=Released`, then `complete` + - org-scoped `prod:` deploys (e.g. `prod:demonstration`, `prod:apple`) are skipped — only the wildcard prod deploy represents a release event - `dev` deploys are skipped + - the step is failure-tolerant (`|| echo …`); a Linear API or download failure does not fail the deploy job Supported deploy environments: - `dev:` @@ -175,7 +176,7 @@ Additional CircleCI secrets for the QA2 E2E dispatch step: CircleCI context for the Linear release steps: - `linear-secrets` context, providing `LINEAR_ACCESS_KEY` (Linear release pipeline access key) -- attached to the `release-artifact` workflow in [`.circleci/config.yml`](../.circleci/config.yml) (sync + `Started` stage on tag build) and to both deploy workflows in [`.circleci/deploy.yml`](../.circleci/deploy.yml) (per-stage `update` and final `complete`) +- attached to the `release-artifact` workflow in [`.circleci/config.yml`](../.circleci/config.yml) (sync + `Started` stage on tag build), and to the `deploy-qa` and `deploy-prod` workflows in [`.circleci/deploy.yml`](../.circleci/deploy.yml). The `deploy-dev` workflow does not have access to the Linear secret. - the Linear release pipeline is configured as **scheduled**; stages used: built-in `Started`, custom `QA` (frozen) and `Sandbox`, and built-in terminal `Released`. CI also calls `complete` after a successful prod deploy. For QA deploys that include `qa2`, [`.circleci/deploy.yml`](../.circleci/deploy.yml) resolves the release SHA, passes the release tag, SHA, and a CircleCI run URL to [`scripts/dispatch-qa2-e2e.js`](../scripts/dispatch-qa2-e2e.js), and that script uses the GitHub App credentials above plus the `app-tests` installation id to mint a short-lived installation token before posting `repository_dispatch` with this payload: diff --git a/scripts/download-linear-release.sh b/scripts/download-linear-release.sh new file mode 100755 index 00000000..f990f9b3 --- /dev/null +++ b/scripts/download-linear-release.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Download and verify the linear/linear-release CLI to /tmp/linear-release. +# Single source of truth for the pinned version + sha256 used by both +# .circleci/config.yml (release-artifact workflow) and .circleci/deploy.yml +# (deploy workflows). Prints the binary path on success. + +set -euo pipefail + +linear_release_version="v0.7.0" +linear_release_sha256="c82e10e79ac54bfa5efff69124add2aa793d91b0d5e32c1ed56ab856eb2a7e79" +linear_release_url="https://github.com/linear/linear-release/releases/download/${linear_release_version}/linear-release-linux-x64" +linear_release_bin="${LINEAR_RELEASE_BIN:-/tmp/linear-release}" + +curl -fsSL "$linear_release_url" -o "$linear_release_bin" +echo "${linear_release_sha256} ${linear_release_bin}" | sha256sum -c - +chmod +x "$linear_release_bin" +echo "$linear_release_bin" From 50eb96ae3d5549e51c6c0d9ec34bad03c5e22a2b Mon Sep 17 00:00:00 2001 From: Paul Falgout Date: Sat, 2 May 2026 01:05:41 +0900 Subject: [PATCH 3/3] ci(linear): chain commands with && to surface failures set -e is suspended on the left side of ||, so the previous { cmd1; cmd2; cmd3; } || echo block silently swallowed earlier command failures: the group exit status came from the trailing if/last command, not from any failed step in the middle. Chain commands with && so the first failure short-circuits and the fallback echo fires. The trailing prod-only complete is encoded as { [ stage != prod ] || complete; } so its exit status reflects whether complete actually ran successfully. Co-Authored-By: Claude Opus 4.7 --- .circleci/config.yml | 18 ++++++++++-------- .circleci/deploy.yml | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a8be0b7..d101422d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -279,14 +279,16 @@ jobs: when: on_success command: | set -uo pipefail - { - ./scripts/download-linear-release.sh - git fetch --force --tags origin - /tmp/linear-release sync \ - --release-version="<< pipeline.git.tag >>" - /tmp/linear-release update \ - --release-version="<< pipeline.git.tag >>" \ - --stage="Started" + # set -e is suspended on the left side of `||`, so chain commands + # with `&&` to short-circuit on the first failure and let the + # group's exit code reflect the actual outcome. + { ./scripts/download-linear-release.sh \ + && git fetch --force --tags origin \ + && /tmp/linear-release sync \ + --release-version="<< pipeline.git.tag >>" \ + && /tmp/linear-release update \ + --release-version="<< pipeline.git.tag >>" \ + --stage="Started" } || echo "Linear release sync failed; artifact published. Sync manually if needed." # ------------------------------------------------------------ diff --git a/.circleci/deploy.yml b/.circleci/deploy.yml index 771a3d4d..ac8e7e53 100644 --- a/.circleci/deploy.yml +++ b/.circleci/deploy.yml @@ -275,16 +275,17 @@ jobs: *) exit 0 ;; esac - { - ./scripts/download-linear-release.sh - git fetch --force --tags origin - /tmp/linear-release update \ - --release-version="$DEPLOY_TARGET_VERSION" \ - --stage="$linear_stage" - if [ "$DEPLOY_STAGE" = "prod" ]; then - /tmp/linear-release complete \ - --release-version="$DEPLOY_TARGET_VERSION" - fi + # set -e is suspended on the left side of `||`, so chain commands + # with `&&` to short-circuit on the first failure and let the + # group's exit code reflect the actual outcome. + { ./scripts/download-linear-release.sh \ + && git fetch --force --tags origin \ + && /tmp/linear-release update \ + --release-version="$DEPLOY_TARGET_VERSION" \ + --stage="$linear_stage" \ + && { [ "$DEPLOY_STAGE" != "prod" ] \ + || /tmp/linear-release complete \ + --release-version="$DEPLOY_TARGET_VERSION"; } } || echo "Linear release update failed; deploy succeeded. Sync manually if needed." workflows: