This implementation addresses four CI/CD reliability and security issues discovered in the GitHub Actions workflows and Terraform configuration:
- Issue 1 (🟠 High / Security): API container image scan silently no-ops because it targets a Dockerfile path that doesn't exist
- Issue 2 (🟠 High / Reliability):
terraform plannever converges becausedefault_tagsincludes a constantly-changingtimestamp()value - Issue 3 (🟡 Medium / Reliability): Rust dependency audit only fails CI on critical advisories, letting high-severity CVEs through
- Issue 4 (🟡 Medium / Security): Workflows depend on archived/unmaintained third-party
GitHub Actions (
actions-rs/toolchain,actions/create-release)
All changes are in a single branch: fix/ci-security-reliability-issues
Files Modified: .github/workflows/test.yml
Problem:
The container-scanning job's build step ran:
docker build -t predictiq-api:${{ github.sha }} -f services/api/Dockerfile .services/api/Dockerfile does not exist anywhere in the repo — only the root Dockerfile and
services/tts/Dockerfile do. The build step was also marked continue-on-error: true, and the
following Trivy scan step was gated on if: success(). The net effect: the build failed on
every run, the failure was swallowed, the Trivy scan for the API image never executed, and the
job still reported green.
Fix:
- Build now points at the real Dockerfile:
-f Dockerfile(repo root context, where the actual API Dockerfile lives). - Removed
continue-on-error: truefrom the build step, so a genuine build failure fails the job instead of being swallowed. - Removed the
if: success()gate on the Trivy step — withcontinue-on-errorgone, a failed build now naturally halts the job before Trivy runs, and a successful build always reaches the scan.
Before:
- name: Build API container image
run: docker build -t predictiq-api:${{ github.sha }} -f services/api/Dockerfile .
continue-on-error: true
- name: Scan API container with Trivy
uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # master
if: success()
with:
image-ref: "predictiq-api:${{ github.sha }}"
...After:
- name: Build API container image
run: docker build -t predictiq-api:${{ github.sha }} -f Dockerfile .
- name: Scan API container with Trivy
uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # master
with:
image-ref: "predictiq-api:${{ github.sha }}"
...Validation (to run before merge):
docker build -t predictiq-api:test -f Dockerfile .Re-run the container-scanning job in test.yml and confirm the Trivy step actually executes
and reports findings.
Files Modified: infrastructure/terraform/main.tf
Problem:
provider "aws" {
default_tags {
tags = {
Environment = var.environment
Project = "predictiq"
ManagedBy = "terraform"
CreatedAt = timestamp()
}
}
}default_tags applies to every taggable resource created by the AWS provider. Because
timestamp() re-evaluates on every terraform plan/apply invocation, every taggable resource
showed a spurious in-place tag update on every single run — breaking the ability to get a clean
terraform plan, which terraform.yml uses as a CI gate, and risking unnecessary resource churn
on every deploy.
Fix:
Removed CreatedAt = timestamp() from default_tags entirely. No replacement mechanism was
added (e.g. ignore_changes or a per-resource tag set once at creation) since no downstream
consumer of a CreatedAt tag was found in the codebase — if one is needed later, it should be
set via a separate mechanism that doesn't recompute on every plan.
Before:
default_tags {
tags = {
Environment = var.environment
Project = "predictiq"
ManagedBy = "terraform"
CreatedAt = timestamp()
}
}After:
default_tags {
tags = {
Environment = var.environment
Project = "predictiq"
ManagedBy = "terraform"
}
}Note: infrastructure/terraform/modules/rds/main.tf also calls timestamp(), but only
inside a conditional final_snapshot_identifier string for prod, which is not part of
default_tags and does not cause plan drift on every run — left unchanged.
Validation (to run before merge):
terraform plan -var-file=environments/dev.tfvars
terraform plan -var-file=environments/dev.tfvars # run twiceThe second run should report No changes., and no resource tag diff should include a
CreatedAt/timestamp value.
Files Modified: .github/workflows/dependency-scan.yml
Problem:
The scan-rust job ran cargo audit --deny warnings with continue-on-error: true, then a
follow-up step parsed the JSON output and only exited non-zero when
.advisory.severity == "critical" was found. High-severity Rust CVEs were logged to the console
but never failed the build. This was inconsistent with:
- The
npm auditstep in the same file, which fails on bothcriticalandhigh. test.yml'scontracts-crateaudit, which runscargo audit --deny warningswith nocontinue-on-errorat all.
Fix:
- Removed
continue-on-error: truefrom both initialcargo audit --deny warningssteps (contracts/predict-iqandservices/api), so any warning-level finding fails the job immediately, matching the behavior already used intest.yml. - Extended both follow-up JSON-based severity checks to count and fail on
highseverity in addition tocritical, so the Rust audit gate matches the npm audit gate's severity threshold.
Before:
- name: Audit contracts/predict-iq dependencies
run: cargo audit --deny warnings
working-directory: contracts/predict-iq
continue-on-error: true
- name: Audit contracts/predict-iq (fail on critical)
run: |
output=$(cargo audit --json)
critical=$(echo "$output" | jq '[.vulnerabilities[] | select(.advisory.severity == "critical")] | length')
if [ "$critical" -gt 0 ]; then
echo "❌ Found $critical critical vulnerabilities in contracts/predict-iq"
exit 1
fi
working-directory: contracts/predict-iqAfter:
- name: Audit contracts/predict-iq dependencies
run: cargo audit --deny warnings
working-directory: contracts/predict-iq
- name: Audit contracts/predict-iq (fail on critical or high)
run: |
output=$(cargo audit --json)
critical=$(echo "$output" | jq '[.vulnerabilities[] | select(.advisory.severity == "critical")] | length')
high=$(echo "$output" | jq '[.vulnerabilities[] | select(.advisory.severity == "high")] | length')
if [ "$critical" -gt 0 ] || [ "$high" -gt 0 ]; then
echo "❌ Found $critical critical and $high high vulnerabilities in contracts/predict-iq"
exit 1
fi
working-directory: contracts/predict-iqThe same change was applied symmetrically to the services/api audit steps.
Validation (to run before merge):
cd services/api && cargo audit --deny warningsagainst a dependency with a known high-severity advisory, and confirm it exits non-zero. Re-run
dependency-scan.yml and confirm the job fails (not just logs a warning) on a high-severity
finding.
Files Modified: .github/workflows/contract-deployment.yml, .github/workflows/release.yml,
.github/workflows/test.yml, .github/workflows/performance.yml
Problem:
actions-rs/toolchain@v1was used in five places (contract-deployment.yml:35,release.yml:88,test.yml:497,performance.yml:47and230). Theactions-rsGitHub org has been archived since 2021 and receives no further updates or security patches.actions/create-release@v1was used incontract-deployment.yml:239. This action was deprecated by GitHub in favor ofsoftprops/action-gh-release, whichrelease.ymlalready uses twice in the same repo (lines 65 and 132).- Both are inconsistent with the actively-maintained
dtolnay/rust-toolchainaction already used independency-scan.yml.
Fix:
- Replaced every
actions-rs/toolchain@v1step withdtolnay/rust-toolchain@stable, translating the oldprofile: minimal/toolchain: stable/override: true/target: ...inputs to the equivalenttargets:/components:inputsdtolnay/rust-toolchainexpects (droppingprofileandoverride, which have no equivalent and aren't needed —dtolnay/rust-toolchainsets the installed toolchain as default automatically). - Replaced
contract-deployment.yml'sactions/create-release@v1step withsoftprops/action-gh-release@v2, translatingrelease_name→nameand keepingtag_name/body/draft/prereleaseas-is, matching the pattern already used twice inrelease.yml.
Before (example from contract-deployment.yml):
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: wasm32-unknown-unknown
components: rustfmt, clippyAfter:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippyBefore (contract-deployment.yml release creation):
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: contract-${{ needs.build-and-test.outputs.contract-hash }}
release_name: Contract Deployment ${{ needs.build-and-test.outputs.contract-hash }}
body: |
Mainnet deployment of PredictIQ contract
...
draft: false
prerelease: falseAfter:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: contract-${{ needs.build-and-test.outputs.contract-hash }}
name: Contract Deployment ${{ needs.build-and-test.outputs.contract-hash }}
body: |
Mainnet deployment of PredictIQ contract
...
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Validation (to run before merge):
grep -rn "actions-rs/toolchain\|actions/create-release" .github/workflowsThis was run locally after the change and returns zero matches. Trigger release.yml and
contract-deployment.yml and confirm both complete successfully end-to-end with the replacement
actions.
None of the changes in this branch were built, run, or executed locally — per the task constraints, only the workflow YAML and Terraform HCL files were edited. Before merge, the Testing Requirements listed in each original issue should be run:
docker build -t predictiq-api:test -f Dockerfile .from repo rootterraform plan -var-file=environments/dev.tfvarstwice in a row against unchanged configcd services/api && cargo audit --deny warningsagainst a known high-severity advisorygrep -rn "actions-rs/toolchain\|actions/create-release" .github/workflows(zero matches confirmed already)hadolint Dockerfileandactionlintagainst the modified workflow files- End-to-end trigger of
release.ymlandcontract-deployment.yml
-
fix(ci): point API container scan at repo-root Dockerfile Fixes the container-scanning job building a non-existent Dockerfile path and silently skipping the Trivy scan.
-
fix(terraform): remove timestamp() from provider default_tags Fixes
terraform plannever converging due to a constantly-changing tag value. -
fix(ci): fail dependency-scan on high-severity Rust advisories Fixes the Rust dependency audit only failing CI on critical (not high) severity findings.
-
fix(ci): replace archived actions-rs/toolchain and actions/create-release Replaces unmaintained third-party actions with actively-maintained equivalents already used elsewhere in the repo.
All changes are in: fix/ci-security-reliability-issues
Ready for a PR that closes all four issues.
- ✅ API container scan build path corrected,
continue-on-error/if: success()gating removed - ✅ Terraform
default_tagsno longer includestimestamp() - ✅ Rust dependency audit fails on
criticalandhighseverity, consistently acrosscontracts/predict-iqandservices/api - ✅
actions-rs/toolchainandactions/create-releasefully removed from all workflows (confirmed viagrep) - ✅ Four issues, four isolated commits, single branch
- ⬜
docker build,terraform plan,cargo audit,hadolint, andactionlintvalidation runs — not executed as part of this change, still required before merge