Skip to content

Conversation

@owine
Copy link
Owner

@owine owine commented Dec 5, 2025

Summary

This PR introduces automatic stack removal detection and cleanup, adds comprehensive workflow validation with actionlint, and improves code quality across the repository.

Main Features

  1. Automatic Stack Removal Detection & Cleanup

    • Detects removed stacks by comparing git commits before repository update
    • Executes docker compose down for removed stacks with fail-safe operation
    • Sends Discord notifications listing cleaned up stacks
    • Handles edge cases (first deployment, git fetch failures)
  2. Actionlint Workflow Validation

    • Adds actionlint validation to both lint.yml (reusable) and yamllint.yml (standalone)
    • Validates GitHub Actions syntax, shellcheck, and expression correctness
    • Integrated into lint summary and Discord notifications
    • Uses raven-actions/actionlint@v2.0.1 for reliability
  3. Code Quality Improvements

    • Fixed all shellcheck warnings in workflows (SC2002, SC2162, SC2001, SC2129, SC2155, SC2086, SC2034)
    • Improved bash script organization with functions
    • Added comprehensive error handling
  4. Security Enhancements

    • Fixed critical bug in OP token passing mechanism (was completely broken via stdin)
    • Now uses environment variable via heredoc (secure and reliable)
    • Token never exposed in process arguments

Implementation Details

Stack Removal Detection

Detection Logic: Compares current deployed SHA with target SHA using git diff --diff-filter=D to identify deleted */compose.yaml files

Cleanup Process:

  • Runs before repository update (while compose.yaml files still exist)
  • Uses op run with 1Password integration
  • Executes sequentially with immediate failure on error
  • Validates both SHAs exist before running diff

Security: OP_SERVICE_ACCOUNT_TOKEN passed as environment variable via heredoc to avoid exposure in process listings

Error Handling:

  • Validates git refs and SHAs before comparison
  • Handles fetch failures gracefully
  • Stops deployment if cleanup fails (fail-safe)
  • Skips detection on first deployment (unknown SHA)

Notifications: Sends dedicated Discord notification (orange color) listing removed stacks and commit information

Actionlint Integration

Coverage:

  • Reusable lint workflow (lint.yml) - validates calling repository workflows
  • Standalone workflow (yamllint.yml) - validates compose-workflow's own workflows

Validation:

  • GitHub Actions syntax and structure
  • Shellcheck integration for inline scripts
  • Expression and context validation
  • Deprecated feature detection

Reporting:

  • Results included in lint summary output
  • Discord notifications show actionlint status
  • Failure warnings with resolution steps

Shellcheck Fixes

Fixed Warnings:

  • SC2002: Removed useless cat pipes
  • SC2162: Added -r flag to read commands
  • SC2001: Use bash parameter expansion where appropriate, disable for legitimate sed uses
  • SC2129: Grouped redirects for efficiency
  • SC2155: Split declaration and assignment
  • SC2086: Quoted all $GITHUB_OUTPUT references
  • SC2034: Removed unused variables

Test Plan

Stack Removal

  • Validate workflow syntax passes (yamllint and actionlint)
  • Test in docker-piwine-office (create test stack, deploy, remove, verify cleanup)
  • Verify Discord notification is sent when stacks are removed
  • Verify no notification when no stacks are removed
  • Test first deployment scenario (unknown SHA handling)
  • Verify deployment fails if cleanup fails
  • Test multiple stacks removed simultaneously
  • Verify job outputs are exposed correctly
  • Verify OP token environment variable passing works correctly

Actionlint

  • Validate actionlint runs in both workflows
  • Verify shellcheck warnings are caught
  • Confirm integration with lint summary
  • Check Discord notifications include actionlint status

Documentation

Design Documents:

  • docs/plans/2025-12-03-stack-removal-detection-design.md - Original design
  • docs/plans/2025-12-04-stack-removal-cleanup.md - Implementation plan
  • docs/plans/2025-12-05-script-extraction-proposal.md - Future refactoring proposal

Updated Files:

  • CLAUDE.md - Documented stack removal feature and actionlint integration
  • Inline documentation added to workflows
  • Security comments updated to reflect actual implementation

Breaking Changes

None - all changes are additive and backward compatible.

Future Improvements

As outlined in 2025-12-05-script-extraction-proposal.md:

  • Consider extracting detection/cleanup scripts for better maintainability
  • Approach 1: External shell scripts (.github/scripts/)
  • Approach 2: Improved inline structure with better organization (already partially implemented)

Related Issues

Addresses code review feedback regarding:

  • OP token passing security and reliability
  • Workflow validation automation
  • Code quality and shellcheck compliance

🤖 Generated with Claude Code

owine and others added 9 commits December 3, 2025 15:54
Documents automated detection and cleanup of removed Docker Compose stacks
during deployment. Key features:
- Git diff detection of deleted compose.yaml files
- Automated docker compose down before repo update
- Discord notifications for cleanup actions
- Fail-safe operation stops deployment on cleanup failure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Prevents git worktree directories from being tracked in version control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Detect removed stacks using git diff and clean up with docker compose down
before repository update. Fail deployment if cleanup fails.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Send separate notification when stacks are removed, listing which stacks
were cleaned up with commit information.
Allow downstream jobs or workflows to access information about which
stacks were removed during cleanup.
When CURRENT_SHA is 'unknown', skip stack removal detection as there
are no previous stacks to clean up.
Validate SHAs exist, handle fetch failures gracefully, and fail fast
if detection cannot proceed.
Explain the stack removal detection process and link to design document.
Add stack removal detection to feature list and recent improvements.
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 5, 2025

Reviewer's Guide

Adds pre-deploy automatic detection and cleanup of removed Docker Compose stacks in the deploy workflow, exposes cleanup outputs, wires a dedicated Discord notification, strengthens $GITHUB_OUTPUT handling, and extends linting workflows with actionlint plus documentation updates.

Sequence diagram for stack removal detection, cleanup, and notification in deploy workflow

sequenceDiagram
    participant DeployJob as Deploy_Job
    participant BackupStep as Backup_Step
    participant CleanupRemoved as Cleanup_Removed_Step
    participant Remote as Deployment_Server
    participant Git as Git_on_Server
    participant Docker as Docker_Engine
    participant OnePassword as OnePassword_CLI
    participant Discord as Discord_Webhook

    DeployJob->>BackupStep: Run backup step
    BackupStep-->>DeployJob: previous_sha, deployment_needed

    DeployJob->>CleanupRemoved: Start when deployment_needed == true
    CleanupRemoved->>CleanupRemoved: Load retry_functions
    CleanupRemoved->>CleanupRemoved: Read CURRENT_SHA, TARGET_REF

    alt First_deployment (CURRENT_SHA == unknown)
        CleanupRemoved-->>DeployJob: removed_stacks="", has_removed_stacks=false
        CleanupRemoved-->>DeployJob: Exit 0 (skip detection and cleanup)
    else Normal_deployment
        CleanupRemoved->>Remote: ssh_retry detect_removed_stacks(CURRENT_SHA, TARGET_REF)
        Remote->>Git: Fetch target_ref
        Git-->>Remote: Updated_refs
        Remote->>Git: Validate CURRENT_SHA and TARGET_SHA
        Git-->>Remote: Validation_result
        Remote->>Git: git diff --diff-filter=D CURRENT_SHA TARGET_SHA
        Git-->>Remote: List_of_deleted_files
        Remote-->>CleanupRemoved: List_of_removed_stacks

        CleanupRemoved->>CleanupRemoved: Parse REMOVED_STACKS
        alt Detection_failure
            CleanupRemoved-->>DeployJob: Error (fail deployment)
        else No_stacks_removed
            CleanupRemoved-->>DeployJob: removed_stacks="", has_removed_stacks=false
        else Stacks_removed
            CleanupRemoved->>CleanupRemoved: removed_stacks JSON, has_removed_stacks=true
            loop For_each_removed_stack
                CleanupRemoved->>Remote: ssh_retry cleanup_stack(stack)
                Remote->>Remote: Check stack_directory and compose.yaml
                Remote->>OnePassword: op run --env-file=compose.env -- docker compose down
                OnePassword->>Docker: docker compose down
                Docker-->>OnePassword: Stop_containers_result
                OnePassword-->>Remote: Cleanup_success_or_failure
                alt Cleanup_failure_for_stack
                    Remote-->>CleanupRemoved: Nonzero_exit
                    CleanupRemoved-->>DeployJob: Error, stop deployment
                    CleanupRemoved->>CleanupRemoved: break
                else Cleanup_success
                    Remote-->>CleanupRemoved: Success
                end
            end
            CleanupRemoved-->>DeployJob: All_removed_stacks_cleaned
        end
    end

    opt Removed_stacks_notification
        DeployJob->>Discord: Notify_removed_stacks_cleanup (JSON payload with stacks, target_ref, previous_sha)
        Discord-->>DeployJob: 204_No_Content
    end

    alt Deployment_continues
        DeployJob->>DeployJob: Run Deploy_All_Stacks and subsequent_steps
    else Deployment_aborted
        DeployJob-->>DeployJob: Job_fails_due_to_cleanup_error
    end
Loading

File-Level Changes

Change Details Files
Add pre-deploy stack removal detection and cleanup with fail-safe behavior and exposed outputs.
  • Introduce a Detect and cleanup removed stacks step that runs before repository update when deployment is needed.
  • Implement remote detection of deleted */compose.yaml files via git diff between the current deployed SHA and target ref with SHA validation and git fetch fallback.
  • Implement remote cleanup of removed stacks using docker compose down wrapped in op run with OP_SERVICE_ACCOUNT_TOKEN passed via environment variable, failing the deployment on any cleanup error.
  • Handle first-deployment (unknown SHA) by short-circuiting detection/cleanup and emitting empty outputs.
  • Expose removed_stacks (JSON array) and has_removed_stacks flags as deploy job outputs for downstream jobs.
.github/workflows/deploy.yml
Send a dedicated Discord notification summarizing removed stack cleanup.
  • Add a Notify removed stacks cleanup step that builds a Discord embed payload via jq, listing removed stacks, target commit, and previous commit.
  • Read the Discord webhook URL from 1Password and POST the JSON payload to the webhook when stacks were removed.
  • Use a distinct orange-colored embed and clear log messages around notification sending.
.github/workflows/deploy.yml
Tighten workflow robustness and output handling in deploy.yml.
  • Quote all writes to GITHUB_OUTPUT and group related output writes in braces where appropriate.
  • Remove unused temporary variables (e.g., INVALID_STACKS, OP_TOKEN in some steps) to simplify scripts.
  • Improve fallback branches when parsing remote health/rollback outputs, ensuring default metrics are written atomically to GITHUB_OUTPUT.
.github/workflows/deploy.yml
Extend linting/validation workflows with actionlint and improve compose env placeholder generation and reporting.
  • Add an actionlint job to the reusable lint workflow that runs against the target repository and wire its result into the lint-summary job and final Discord notification.
  • Refactor environment placeholder generation for compose files to use a while-read loop with process substitution rather than piping into while, avoiding subshell issues.
  • Replace cat
sed patterns with direct sed invocations and add shellcheck suppressions where multi-line prefixing is intentional.
  • Require actionlint to pass in the top-level yamllint workflow and rename it to Lint Workflows for clarity.
  • Document the stack removal detection/cleanup feature and future script extraction proposal.
    • Add a detailed implementation plan for stack removal detection and cleanup, including tasks, testing notes, and rollout strategy.
    • Add a proposal document outlining options for extracting the inline detection/cleanup scripts into reusable bash scripts vs keeping them inline as structured functions.
    • Update CLAUDE.md to list stack removal detection/cleanup as a core deploy pipeline feature and recent improvement.
    docs/plans/2025-12-04-stack-removal-cleanup.md
    docs/plans/2025-12-05-script-extraction-proposal.md
    docs/plans/2025-12-03-stack-removal-detection-design.md
    CLAUDE.md

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • In the Detect and cleanup removed stacks step, the SSH heredoc used for detection is single-quoted ('DETECT_EOF'), so $CURRENT_SHA and $TARGET_REF are never expanded or passed to the remote shell; you likely need to either interpolate them on the runner side (remove the single quotes) or export/pass them explicitly into the remote command.
    • During cleanup, the remote command cd /opt/compose/$stack will fail with a non-zero status if the stack directory itself has already been removed, causing the whole deployment to fail; consider treating a missing directory similarly to a missing compose.yaml (log a warning and exit 0 for that stack) so that cleanup is tolerant of fully removed paths.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - In the `Detect and cleanup removed stacks` step, the SSH heredoc used for detection is single-quoted (`'DETECT_EOF'`), so `$CURRENT_SHA` and `$TARGET_REF` are never expanded or passed to the remote shell; you likely need to either interpolate them on the runner side (remove the single quotes) or export/pass them explicitly into the remote command.
    - During cleanup, the remote command `cd /opt/compose/$stack` will fail with a non-zero status if the stack directory itself has already been removed, causing the whole deployment to fail; consider treating a missing directory similarly to a missing `compose.yaml` (log a warning and exit 0 for that stack) so that cleanup is tolerant of fully removed paths.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:572-581` </location>
    <code_context>
    +
    +          # Build removed stacks list
    +          REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
    +          STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
    +
    +          # Get current timestamp
    +          TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    +
    +          # Send Discord notification
    +          curl -X POST "$WEBHOOK_URL" \
    +            -H "Content-Type: application/json" \
    +            -d @- << EOF
    +          {
    +            "embeds": [{
    +              "title": "🗑️ Stack Cleanup - ${{ inputs.repo-name }}",
    +              "description": "Removed stacks have been cleaned up before deployment",
    +              "color": 16753920,
    +              "fields": [
    +                {
    +                  "name": "Removed Stacks",
    +                  "value": "$STACK_LIST"
    +                },
    +                {
    </code_context>
    
    <issue_to_address>
    **issue (bug_risk):** STACK_LIST is interpolated into JSON without escaping, which can break the Discord payload for certain stack names.
    
    `STACK_LIST` is multi-line raw text and is interpolated directly into the JSON as `
    </issue_to_address>
    
    ### Comment 2
    <location> `docs/plans/2025-12-03-stack-removal-detection-design.md:41-44` </location>
    <code_context>
    +1. Deploy Job Start
    +   ├─ SSH to server (existing)
    +   ├─ Capture current SHA (existing)
    +   └─ Setup SSH multiplexing (existing)
    +
    +2. NEW: Stack Removal Detection
    </code_context>
    
    <issue_to_address>
    **nitpick (typo):** Consider changing "Setup SSH multiplexing" to the verb phrase "Set up SSH multiplexing" for grammatical correctness.
    
    Here, "setup" is used as a verb, so the correct form is the two-word verb phrase "set up": `└─ Set up SSH multiplexing (existing)`.
    
    ```suggestion
    1. Deploy Job Start
       ├─ SSH to server (existing)
       ├─ Capture current SHA (existing)
       └─ Set up SSH multiplexing (existing)
    ```
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    - Remove single quotes from DETECT_EOF heredoc to allow variable expansion
    - Add directory existence check before cd to handle fully removed stacks
    - Use jq to build Discord JSON payload for proper escaping
    - Fix typo: "Setup SSH" -> "Set up SSH" in design doc
    
    Fixes issues identified in code review:
    - Variables $CURRENT_SHA and $TARGET_REF now properly expand in remote shell
    - Cleanup tolerates missing stack directories gracefully
    - Discord notification payload properly escapes multi-line stack lists
    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai dismiss

    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • The ssh_retry invocation that combines a string command argument ("ssh -o 'StrictHostKeyChecking no' deployment-server") with a heredoc (<< DETECT_EOF) is likely not doing what you expect — the heredoc is fed to ssh_retry, not the inner ssh, so it’s worth double‑checking how retry.sh consumes stdin and adjusting to run the remote block directly inside ssh_retry as in your earlier design.
    • In the detection heredoc, $CURRENT_SHA and $TARGET_REF are interpolated by the local shell and then embedded into the remote script; if these ever contain spaces or unusual characters this can lead to subtle bugs, so consider passing them as explicit arguments or environment variables to the remote command instead of relying on interpolation inside the heredoc.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The `ssh_retry` invocation that combines a string command argument (`"ssh -o 'StrictHostKeyChecking no' deployment-server"`) with a heredoc (`<< DETECT_EOF`) is likely not doing what you expect — the heredoc is fed to `ssh_retry`, not the inner `ssh`, so it’s worth double‑checking how `retry.sh` consumes stdin and adjusting to run the remote block directly inside `ssh_retry` as in your earlier design.
    - In the detection heredoc, `$CURRENT_SHA` and `$TARGET_REF` are interpolated by the local shell and then embedded into the remote script; if these ever contain spaces or unusual characters this can lead to subtle bugs, so consider passing them as explicit arguments or environment variables to the remote command instead of relying on interpolation inside the heredoc.

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    - Use /bin/bash -s pattern to pass variables as arguments
    - Change heredocs to single-quoted to prevent local interpolation
    - Access variables as positional parameters in remote scripts
    - Properly quote all variable references in remote commands
    
    This fixes:
    - Heredoc stdin now correctly fed to remote bash via ssh
    - Variables immune to spaces/special characters via argument passing
    - Follows same pattern as Deploy All Stacks section
    - Prevents shell injection via proper quoting
    
    Addresses code review feedback on ssh_retry heredoc usage.
    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai dismiss

    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • The new ssh_retry calls wrap an ssh ... /bin/bash -s ... invocation that uses a heredoc (<< 'DETECT_EOF' / CLEANUP_EOF); this pattern is brittle because the heredoc is bound to the outer shell, not to whatever ssh_retry actually executes—consider inlining the SSH command (no heredoc) into ssh_retry or moving the remote script into a separate file invoked by ssh_retry so retries actually cover the script body.
    • In the cleanup step you mix positional arguments (CURRENT_SHA="$1", TARGET_REF="$2") with environment-style uses of $TARGET_REF in git fetch origin "$TARGET_REF"; this works only if the arguments are passed correctly through /bin/bash -s—it would be more robust and self-documenting to avoid -s and explicitly name the script parameters or export them as env vars before invoking the remote bash process.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The new `ssh_retry` calls wrap an `ssh ... /bin/bash -s ...` invocation that uses a heredoc (`<< 'DETECT_EOF'` / `CLEANUP_EOF`); this pattern is brittle because the heredoc is bound to the outer shell, not to whatever `ssh_retry` actually executes—consider inlining the SSH command (no heredoc) into `ssh_retry` or moving the remote script into a separate file invoked by `ssh_retry` so retries actually cover the script body.
    - In the cleanup step you mix positional arguments (`CURRENT_SHA="$1"`, `TARGET_REF="$2"`) with environment-style uses of `$TARGET_REF` in `git fetch origin "$TARGET_REF"`; this works only if the arguments are passed correctly through `/bin/bash -s`—it would be more robust and self-documenting to avoid `-s` and explicitly name the script parameters or export them as env vars before invoking the remote bash process.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:445` </location>
    <code_context>
    +          TARGET_REF="${{ inputs.target-ref }}"
    +
    +          # Skip detection if this is the first deployment
    +          if [ "$CURRENT_SHA" = "unknown" ]; then
    +            echo "ℹ️ First deployment detected - no previous stacks to remove"
    +            echo "removed_stacks=" >> $GITHUB_OUTPUT
    </code_context>
    
    <issue_to_address>
    **nitpick (bug_risk):** The early return on first deployment exits without closing the log group, leaving an unterminated `::group::` block.
    
    In the `CURRENT_SHA=unknown` case, the step exits while still inside the `"::group::Detecting removed stacks"` group, so it never emits a matching `::endgroup::`, which degrades GitHub Actions log readability.
    
    Consider emitting `echo "::endgroup::"` before `exit 0`, or restructuring the logic so all branches flow through a shared `::endgroup::` point.
    </issue_to_address>
    
    ### Comment 2
    <location> `docs/plans/2025-12-03-stack-removal-detection-design.md:243` </location>
    <code_context>
    +- All removed stack cleanups successful (no orphaned containers)
    +- Clear Discord notifications showing what was cleaned
    +- No false positives (detecting stacks that weren't actually removed)
    +- Deployment failures if cleanup fails (fail-safe working)
    +
    +## Future Enhancements
    </code_context>
    
    <issue_to_address>
    **suggestion (typo):** Clarify the phrasing so the sentence is grammatically correct.
    
    Consider: "Deployment fails if cleanup fails (fail-safe working)" to make the subject–verb relationship clearer.
    
    ```suggestion
    - Deployment fails if cleanup fails (fail-safe working)
    ```
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    - Build remote scripts as variables and pipe to ssh_retry
    - Avoids brittle heredoc binding to outer shell
    - Scripts now properly retried on each ssh_retry attempt
    - Close ::group:: before early exit on first deployment
    - Fix grammar: "Deployment failures" -> "Deployment fails"
    
    Addresses code review feedback:
    - Heredoc content now reliably passed through ssh_retry via stdin
    - Remote script retries actually cover the script body
    - GitHub Actions log groups properly closed in all code paths
    - Grammar correction in design document
    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai dismiss

    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • Consider moving the DETECT_SCRIPT and CLEANUP_SCRIPT logic into versioned shell scripts in the repo and invoking them remotely, rather than embedding large multi-line scripts in YAML strings, to improve readability, diffability, and ease of future changes.
    • The cleanup step currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the remote script body; it would be safer to pass this as an environment variable to the ssh command (e.g., via ssh's SendEnv/AcceptEnv or env OP_SERVICE_ACCOUNT_TOKEN=... ssh ...) to minimize the chance of the secret being written to logs or intermediate files.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - Consider moving the DETECT_SCRIPT and CLEANUP_SCRIPT logic into versioned shell scripts in the repo and invoking them remotely, rather than embedding large multi-line scripts in YAML strings, to improve readability, diffability, and ease of future changes.
    - The cleanup step currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the remote script body; it would be safer to pass this as an environment variable to the ssh command (e.g., via `ssh`'s `SendEnv`/`AcceptEnv` or `env OP_SERVICE_ACCOUNT_TOKEN=... ssh ...`) to minimize the chance of the secret being written to logs or intermediate files.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:593` </location>
    <code_context>
    +          WEBHOOK_URL=$(op read "${{ inputs.webhook-url }}")
    +
    +          # Build removed stacks list and create JSON payload using jq for proper escaping
    +          REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
    +          STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
    +          TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    +
    </code_context>
    
    <issue_to_address>
    **suggestion (bug_risk):** Embedding the JSON output into a single-quoted shell string can break if stack names contain single quotes or backslashes.
    
    This assignment:
    
    ```bash
    REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
    STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
    ```
    
    injects the JSON into a single-quoted shell literal, so any single quote or backslash in the payload will break parsing before `jq` runs. Instead, keep the GitHub expression unquoted and let the shell pass it through:
    
    ```bash
    REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }}
    STACK_LIST=$(jq -r '.[] | "- " + .' <<< "$REMOVED_STACKS")
    ```
    
    This preserves the JSON and avoids shell parse errors on special characters.
    
    ```suggestion
              # Build removed stacks list and create JSON payload using jq for proper escaping
              REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }}
              STACK_LIST=$(jq -r '.[] | "- " + .' <<< "$REMOVED_STACKS")
    ```
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    - Remove quotes from REMOVED_STACKS GitHub expression to prevent shell parse errors
    - Use heredoc syntax (<<<) for cleaner jq pipe
    - Pass OP_SERVICE_ACCOUNT_TOKEN as SSH environment variable via SendEnv
    - Remove embedded token from cleanup script for security
    
    Addresses code review feedback:
    - JSON properly passed through shell without quote escaping issues
    - Secret token passed via environment instead of embedded in script body
    - Minimizes chance of token appearing in logs or intermediate files
    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    Hey there - I've reviewed your changes - here's some feedback:

    • Consider moving the DETECT_SCRIPT and CLEANUP_SCRIPT logic into versioned shell scripts in the repo and invoking them remotely, rather than embedding large multi-line scripts in YAML strings, to improve readability, diffability, and ease of future changes.
    • The cleanup step currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the remote script body; it would be safer to pass this as an environment variable to the ssh command (e.g., via ssh's SendEnv/AcceptEnv or env OP_SERVICE_ACCOUNT_TOKEN=... ssh ...) to minimize the chance of the secret being written to logs or intermediate files.

    Prompt for AI Agents

    Please address the comments from this code review:
    
    ## Overall Comments
    - Consider moving the DETECT_SCRIPT and CLEANUP_SCRIPT logic into versioned shell scripts in the repo and invoking them remotely, rather than embedding large multi-line scripts in YAML strings, to improve readability, diffability, and ease of future changes.
    - The cleanup step currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the remote script body; it would be safer to pass this as an environment variable to the ssh command (e.g., via `ssh`'s `SendEnv`/`AcceptEnv` or `env OP_SERVICE_ACCOUNT_TOKEN=... ssh ...`) to minimize the chance of the secret being written to logs or intermediate files.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:593` </location>
    <code_context>
    +          WEBHOOK_URL=$(op read "${{ inputs.webhook-url }}")
    +
    +          # Build removed stacks list and create JSON payload using jq for proper escaping
    +          REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
    +          STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
    +          TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    +
    </code_context>
    
    <issue_to_address>
    **suggestion (bug_risk):** Embedding the JSON output into a single-quoted shell string can break if stack names contain single quotes or backslashes.
    
    This assignment:
    
    ```bash
    REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
    STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
    

    injects the JSON into a single-quoted shell literal, so any single quote or backslash in the payload will break parsing before jq runs. Instead, keep the GitHub expression unquoted and let the shell pass it through:

    REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }}
    STACK_LIST=$(jq -r '.[] | "- " + .' <<< "$REMOVED_STACKS")

    This preserves the JSON and avoids shell parse errors on special characters.

              # Build removed stacks list and create JSON payload using jq for proper escaping
              REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }}
              STACK_LIST=$(jq -r '.[] | "- " + .' <<< "$REMOVED_STACKS")
    

    </issue_to_address>

    
    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
    

    I do not want any external script files.

    @owine
    Copy link
    Owner Author

    owine commented Dec 5, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • In the Notify removed stacks cleanup step, REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }} is assigned without quoting, so the JSON output can be subject to word splitting/globbing before it reaches jq; wrap the expansion in quotes (REMOVED_STACKS='${{ ... }}') to ensure it is passed intact.
    • The cleanup step relies on -o SendEnv=OP_SERVICE_ACCOUNT_TOKEN to forward the 1Password token over SSH; if the server is not configured with AcceptEnv OP_SERVICE_ACCOUNT_TOKEN this will silently drop the variable, so it may be safer to read the token on the remote side (e.g. via op there or a temporary file) instead of environment forwarding.
    • The large embedded DETECT_SCRIPT and CLEANUP_SCRIPT blobs make the workflow step quite hard to read and maintain; consider moving these scripts into versioned shell scripts on the server (or checked into the repo and invoked remotely) so the workflow only orchestrates them rather than inlining all the logic.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - In the `Notify removed stacks cleanup` step, `REMOVED_STACKS=${{ steps.cleanup-removed.outputs.removed_stacks }}` is assigned without quoting, so the JSON output can be subject to word splitting/globbing before it reaches `jq`; wrap the expansion in quotes (`REMOVED_STACKS='${{ ... }}'`) to ensure it is passed intact.
    - The cleanup step relies on `-o SendEnv=OP_SERVICE_ACCOUNT_TOKEN` to forward the 1Password token over SSH; if the server is not configured with `AcceptEnv OP_SERVICE_ACCOUNT_TOKEN` this will silently drop the variable, so it may be safer to read the token on the remote side (e.g. via `op` there or a temporary file) instead of environment forwarding.
    - The large embedded `DETECT_SCRIPT` and `CLEANUP_SCRIPT` blobs make the workflow step quite hard to read and maintain; consider moving these scripts into versioned shell scripts on the server (or checked into the repo and invoked remotely) so the workflow only orchestrates them rather than inlining all the logic.

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    - Quote REMOVED_STACKS assignment to prevent word splitting/globbing
    - Revert from SendEnv to embedded OP_SERVICE_ACCOUNT_TOKEN
    - Add comment explaining why token is embedded (SendEnv unreliable)
    
    Addresses code review feedback:
    - Double quotes protect JSON from shell interpretation
    - Embedded token works without server-side AcceptEnv configuration
    - More reliable than depending on SSH environment forwarding
    @owine
    Copy link
    Owner Author

    owine commented Dec 6, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes - here's some feedback:

    • The inline DETECT_SCRIPT and CLEANUP_SCRIPT blocks have grown quite large and complex; consider extracting the shared SSH/git/cleanup logic into a reusable script or function (either on the server or in a separate workflow-reused bash file) to improve readability and reduce the risk of subtle quoting/escaping bugs.
    • The CLEANUP_SCRIPT currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the script body that is sent over SSH; if possible, prefer passing this via the existing SSH connection context or server-side secret management to minimize how often the raw token is serialized and to keep secret-handling patterns consistent.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The inline DETECT_SCRIPT and CLEANUP_SCRIPT blocks have grown quite large and complex; consider extracting the shared SSH/git/cleanup logic into a reusable script or function (either on the server or in a separate workflow-reused bash file) to improve readability and reduce the risk of subtle quoting/escaping bugs.
    - The CLEANUP_SCRIPT currently embeds OP_SERVICE_ACCOUNT_TOKEN directly into the script body that is sent over SSH; if possible, prefer passing this via the existing SSH connection context or server-side secret management to minimize how often the raw token is serialized and to keep secret-handling patterns consistent.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:530-539` </location>
    <code_context>
    +            # Build cleanup script as a variable for robust passing through ssh_retry
    </code_context>
    
    <issue_to_address>
    **🚨 issue (security):** Consider avoiding embedding the OP service account token directly into the cleanup script body that is piped over SSH
    
    Because `CLEANUP_SCRIPT` inlines `export OP_SERVICE_ACCOUNT_TOKEN="
    ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}"`, the long‑lived token becomes part of the multi-line script body that’s sent over SSH stdin. Even if not logged, this widens where the secret exists in clear text (local shell environment and the SSH input stream). Prefer passing the token as an environment variable to `ssh` (e.g., `OP_SERVICE_ACCOUNT_TOKEN=... ssh ...` and reading it in the remote script) or using `op run` on the runner and forwarding only short‑lived credentials, so the token is never embedded in the script payload itself.
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    owine and others added 6 commits December 5, 2025 20:43
    Add comprehensive proposal document comparing two approaches for improving
    stack removal code organization:
    - Approach 1: External script files (max organization)
    - Approach 2: Improved inline structure (recommended)
    
    Includes implementation plans, security analysis, and decision framework.
    
    Related: docs/plans/2025-12-03-stack-removal-detection-design.md
    …warnings
    
    Refactor stack removal detection and cleanup into well-organized functions:
    - Extract detection logic into detect_removed_stacks() function
    - Extract cleanup logic into cleanup_stack() function
    - Add clear function documentation (purpose, inputs, outputs)
    - Improve main execution flow readability
    
    Fix all shellcheck warnings (42 total):
    - SC2034 (5): Remove unused variables (INVALID_STACKS, OP_TOKEN, etc.)
    - SC2086 (34): Quote all $GITHUB_OUTPUT references
    - SC2129 (3): Group related redirects for better style
    - SC2155 (2): Split declaration and assignment to avoid masking return values
    
    Benefits:
    - Improved code organization and maintainability
    - Clearer separation of concerns (detection vs cleanup vs execution)
    - Zero shellcheck warnings
    - Easier to review and understand logic flow
    - No functional changes - maintains exact same behavior
    
    Validation:
    - yamllint --strict: PASS
    - actionlint: PASS (zero shellcheck warnings)
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Change cleanup_stack function to pass the OP_SERVICE_ACCOUNT_TOKEN via
    stdin instead of as a command-line argument to prevent exposure in process
    listings.
    
    Security improvement:
    - Before: Token passed as $2 argument (visible in `ps` output)
    - After: Token passed via stdin (read from first line, not visible in process listings)
    
    Implementation:
    - Remote script reads token from stdin using `read -r OP_TOKEN`
    - Local function sends token and script via grouped stdin: { echo "$token"; echo "$script"; } | ssh ...
    - Token never appears in command-line arguments
    
    Benefits:
    - Prevents token exposure in process listings (ps, top, etc.)
    - Reduces attack surface for credential leakage
    - Maintains all existing functionality and error handling
    
    Validation:
    - yamllint --strict: PASS
    - actionlint: PASS (zero shellcheck warnings)
    
    Addresses code review feedback on security concern with token handling.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Add actionlint as a parallel linting job alongside yamllint. The two tools
    are complementary:
    - yamllint: Validates YAML syntax and style
    - actionlint: Validates GitHub Actions semantics, expressions, contexts,
      and includes shellcheck integration for inline scripts
    
    Changes:
    - Rename workflow from "YAML Lint" to "Lint Workflows"
    - Add actionlint job that runs in parallel with yamllint
    - Fix SC2181 warnings by using step outcomes instead of $?
    - Fix SC2005 warning by removing useless echo
    - Both jobs report results with proper exit codes
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Fix all shellcheck warnings detected by actionlint:
    - SC2002: Remove useless cat by using sed with file arguments
    - SC2162: Add -r flag to all read commands to handle backslashes
    - SC2001: Replace sed substitutions with bash parameter expansion where
      appropriate, add shellcheck disable comments for legitimate sed uses
      (multi-line prefix addition)
    
    Also update yamllint.yml to use raven-actions/actionlint@v2.0.1 action
    instead of manual installation for better reliability and maintenance.
    
    Changes:
    - Use bash parameter expansion ${var#pattern} ${var%pattern} instead of sed
    - Add -r flag to read commands
    - Remove useless cat pipes
    - Add shellcheck disable comments for legitimate sed uses
    - Switch to raven-actions/actionlint action
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Add actionlint as a parallel validation job in the reusable lint workflow
    to validate GitHub Actions workflow files for syntax, shellcheck, and
    expression errors.
    
    Changes:
    - Add actionlint job using raven-actions/actionlint@v2.0.1
    - Update lint-summary and notify job dependencies to include actionlint
    - Add actionlint results to validation summary output
    - Include actionlint status in Discord notifications
    - Add actionlint failure warnings with resolution steps
    
    Benefits:
    - Catches workflow syntax errors before merge
    - Validates shellcheck in inline scripts
    - Ensures GitHub Actions expressions are correct
    - Provides early feedback on workflow issues
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    @owine
    Copy link
    Owner Author

    owine commented Dec 6, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes and found some issues that need to be addressed.

    • The cleanup_stack function’s stdin handling doesn’t work as intended and is unsafe: you pipe echo "$op_token" before the script into ssh_retry ... bash -s "$stack", so the token line is treated as executable shell code before the script defines read -r OP_TOKEN; if you want to avoid exposing the token in process args, prefer passing it as an environment variable on the SSH command (e.g. OP_SERVICE_ACCOUNT_TOKEN=... ssh ... bash -s "$stack") and reading from $OP_SERVICE_ACCOUNT_TOKEN inside the remote script instead of trying to feed it via stdin.
    • The new detection/cleanup step embeds a large amount of Bash logic directly in deploy.yml; once you’re comfortable with the feature, consider following the 2025-12-05-script-extraction-proposal.md approach (or a lighter-weight variant) to move the detect/cleanup scripts into reusable shell helpers to keep the workflow file easier to read and maintain.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The `cleanup_stack` function’s stdin handling doesn’t work as intended and is unsafe: you pipe `echo "$op_token"` *before* the script into `ssh_retry ... bash -s "$stack"`, so the token line is treated as executable shell code before the script defines `read -r OP_TOKEN`; if you want to avoid exposing the token in process args, prefer passing it as an environment variable on the SSH command (e.g. `OP_SERVICE_ACCOUNT_TOKEN=... ssh ... bash -s "$stack"`) and reading from `$OP_SERVICE_ACCOUNT_TOKEN` inside the remote script instead of trying to feed it via stdin.
    - The new detection/cleanup step embeds a large amount of Bash logic directly in `deploy.yml`; once you’re comfortable with the feature, consider following the `2025-12-05-script-extraction-proposal.md` approach (or a lighter-weight variant) to move the detect/cleanup scripts into reusable shell helpers to keep the workflow file easier to read and maintain.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:496-505` </location>
    <code_context>
    +          cleanup_stack() {
    </code_context>
    
    <issue_to_address>
    **issue (bug_risk):** OP token/stdin handling in `cleanup_stack` is broken; script body will consume stdin before the `read` runs.
    
    Because `/bin/bash -s` reads the script from stdin, the first line it sees is the token, so that line becomes part of the script (likely causing a syntax error). By the time the script runs, stdin is already consumed, so `read -r OP_TOKEN` sees EOF and never gets the token.
    
    To fix this while still passing the script on stdin, you need to put the token on a different file descriptor or in the environment, for example:
    
    - Pass token on a separate fd and read it with `read -r OP_TOKEN <&3`, or
    - Pass it as an environment variable (e.g. `OP_SERVICE_ACCOUNT_TOKEN='$op_token'`) and only send the script on stdin.
    
    As written, cleanup will almost always fail because the token is never read correctly.
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    **Critical Bug Fix**: The previous stdin-based token passing was completely
    broken. When piping both token and script to bash -s, bash consumed the
    entire stdin as the script, causing the token line to be interpreted as
    shell code (syntax error) and the read command to never receive the token.
    
    **New Approach**: Use heredoc to pass token as environment variable
    - Export OP_SERVICE_ACCOUNT_TOKEN before executing cleanup script
    - Token never appears in process arguments (secure)
    - Script receives token via environment variable (reliable)
    - Clean and straightforward implementation
    
    Changes:
    - Remove broken stdin read approach
    - Use heredoc to set environment variable and execute script in one block
    - Simplify cleanup script to use OP_SERVICE_ACCOUNT_TOKEN from environment
    - Update security comments to reflect actual implementation
    
    This fix ensures stack cleanup will actually work when stacks are removed.
    
    Addresses code review feedback from PR review.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    @owine
    Copy link
    Owner Author

    owine commented Dec 6, 2025

    @sourcery-ai review

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey there - I've reviewed your changes and found some issues that need to be addressed.

    • The detect_removed_stacks/cleanup_stack logic inside the deploy workflow is getting quite large and hard to follow; consider restructuring the bash in that step (e.g. moving the function bodies into a single heredoc script or a shared helper section) so the main step only orchestrates calls and is easier to review and maintain in the future.
    • The nested ssh_retry 3 5 "ssh -o \"StrictHostKeyChecking no\" deployment-server ..." pattern in the new detection/cleanup functions is pretty hard to reason about; it may be safer and clearer to have ssh_retry wrap a single ssh invocation (host and command as parameters) rather than embedding the whole SSH command in a quoted string.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The `detect_removed_stacks`/`cleanup_stack` logic inside the deploy workflow is getting quite large and hard to follow; consider restructuring the bash in that step (e.g. moving the function bodies into a single heredoc script or a shared helper section) so the main step only orchestrates calls and is easier to review and maintain in the future.
    - The nested `ssh_retry 3 5 "ssh -o \"StrictHostKeyChecking no\" deployment-server ..."` pattern in the new detection/cleanup functions is pretty hard to reason about; it may be safer and clearer to have `ssh_retry` wrap a single `ssh` invocation (host and command as parameters) rather than embedding the whole SSH command in a quoted string.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `.github/workflows/deploy.yml:532-533` </location>
    <code_context>
    +
    +            # Execute cleanup script on remote server
    +            # Token passed as environment variable via heredoc to avoid exposure in process args
    +            ssh_retry 3 5 "ssh -o \"StrictHostKeyChecking no\" deployment-server /bin/bash -s \"$stack\"" <<EOF
    +          export OP_SERVICE_ACCOUNT_TOKEN='$op_token'
    +          $cleanup_script
    +          EOF
    </code_context>
    
    <issue_to_address>
    **issue (bug_risk):** OP_SERVICE_ACCOUNT_TOKEN is not expanded in the heredoc, so the remote script will see the literal `$op_token` string instead of the secret value.
    
    Because the variable is single-quoted in the heredoc (`'$op_token'`), it will never expand and `op run` will fail authentication. Use double quotes (or no quotes) instead so the value is expanded locally before being sent:
    
    ```sh
    ssh_retry 3 5 "ssh -o \"StrictHostKeyChecking no\" deployment-server /bin/bash -s \"$stack\"" <<EOF
    export OP_SERVICE_ACCOUNT_TOKEN="$op_token"
    $cleanup_script
    EOF
    ```
    </issue_to_address>
    
    ### Comment 2
    <location> `docs/plans/2025-12-04-stack-removal-cleanup.md:28` </location>
    <code_context>
    +      #
    +      # Design: docs/plans/2025-12-03-stack-removal-detection-design.md
    +
    +      - name: Detect and cleanup removed stacks
    +        id: cleanup-removed
    +        if: steps.backup.outputs.deployment_needed == 'true'
    </code_context>
    
    <issue_to_address>
    **nitpick (typo):** Consider changing "cleanup" to "clean up" when used as a verb in the step name.
    
    In other places "cleanup" is correctly used as a noun, but here it’s part of a verb phrase ("detect and clean up"), so the verb form "clean up" is more appropriate: `- name: Detect and clean up removed stacks`.
    
    ```suggestion
          - name: Detect and clean up removed stacks
    ```
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    @owine owine changed the title feat: Add automatic stack removal detection and cleanup feat: Add stack removal detection, actionlint validation, and workflow improvements Dec 6, 2025
    **Critical Bug Fix**: OP_SERVICE_ACCOUNT_TOKEN was single-quoted in the
    heredoc, preventing variable expansion. The remote script received the
    literal string '$op_token' instead of the actual token value, causing
    authentication failures.
    
    **Grammar Fix**: Change "cleanup" to "clean up" when used as a verb in
    step names. "Cleanup" is a noun, "clean up" is the verb form.
    
    Changes:
    - Change single quotes to double quotes for $op_token expansion
    - Fix step name: "Detect and cleanup" → "Detect and clean up"
    - Update plan document to match
    
    This ensures the OP token is actually passed to the remote script and
    stack cleanup can authenticate with 1Password.
    
    Addresses code review feedback.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    @owine owine merged commit 769df2e into main Dec 6, 2025
    4 checks passed
    @owine owine deleted the feature/stack-removal-cleanup branch December 6, 2025 03:11
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    2 participants