Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,42 @@ jobs:
run: |
CGO_ENABLED=1 go build -o dist/devcloud ./cmd/devcloud
CGO_ENABLED=1 go build -o dist/codegen ./cmd/codegen

# internal/generated is committed but derived. The Go tests check the fidelity
# manifest's shape — floors, registered services, the CRUD registry — none of
# which notice an operation a provider gained and the manifest never did.
# Regenerating and diffing is the only check that does.
codegen-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.26"

# Regenerating in place only overwrites the filenames the generator still
# emits — it never removes one it has stopped emitting, and
# internal/codegen/generator.go has no deletion path at all. Clearing the
# tree first turns a retired or renamed output into a visible deletion.
# Every tracked file under internal/generated carries a generated marker,
# so nothing hand-written is lost.
#
# No SQLite headers: cmd/codegen builds with CGO_ENABLED=0.
- name: Regenerate from a clean tree
run: |
rm -rf internal/generated
make codegen

- name: Check the committed output is current
# --porcelain rather than `git diff --exit-code` so an added or removed
# file counts as drift instead of being silently dropped, matching
# smithy-sync.yml's check.
run: |
drift="$(git status --porcelain internal/generated)"
if [ -n "$drift" ]; then
echo "$drift"
echo "::error::internal/generated is stale. Run 'make codegen' and commit the result."
exit 1
fi
197 changes: 186 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,209 @@ env:
HOMEBREW_TAP_REPO: homebrew-tap

jobs:
# A tag is a mutable pointer, and every job below would resolve it
# independently. Force-update or delete-and-recreate it while the run is in
# flight — which is exactly what a maintainer does on spotting that the wrong
# commit got tagged — and the gates vouch for one commit while GoReleaser
# publishes another. Resolving once here and passing the SHA down is what
# makes "the gates passed" a statement about the artefacts.
#
# The resolution has to happen in a job rather than an expression: github.sha
# is immutable but on workflow_dispatch it points at the branch the run was
# launched from, not the tag that was typed in.
resolve:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
sha: ${{ steps.resolve.outputs.sha }}
extra_flags: ${{ steps.resolve.outputs.extra_flags }}
steps:
- name: Checkout the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Pin the tag to a commit
id: resolve
run: |
echo "tag=${{ github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then
echo "extra_flags=--snapshot" >> "$GITHUB_OUTPUT"
fi

# CI triggers on a tag push too, but the two workflows race — nothing stops
# GoReleaser from publishing binaries, images and a Homebrew formula off a red
# commit. These three jobs are what make the tag wait, and they mirror the
# checks in ci.yml rather than trusting that CI got there first.
test:
needs: resolve
strategy:
matrix:
# GoReleaser publishes arm64 artifacts, so the gate covers arm64 too.
runner: [ubuntu-latest, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Comment thread
skyoo2003 marked this conversation as resolved.
with:
ref: ${{ needs.resolve.outputs.sha }}
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Install SQLite dev headers
run: sudo apt-get install -y libsqlite3-dev
- name: Run Go tests
run: CGO_ENABLED=1 go test ./...
Comment thread
skyoo2003 marked this conversation as resolved.

# The Go tests cannot see a stale fidelity manifest — that is the whole reason
# ci.yml has a codegen-drift job — so waiting only on `test` would let a tag
# publish generated code that misstates what the release serves.
codegen-drift:
needs: resolve
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.resolve.outputs.sha }}
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
# Cleared first so a retired or renamed output shows up as a deletion; the
# generator overwrites what it still emits but never removes what it does
# not. See the same step in ci.yml.
- name: Regenerate from a clean tree
run: |
rm -rf internal/generated
make codegen
- name: Check the committed output is current
run: |
drift="$(git status --porcelain internal/generated)"
if [ -n "$drift" ]; then
echo "$drift"
echo "::error::internal/generated is stale on this tag. Run 'make codegen', commit, and re-tag."
exit 1
fi

# compat.yml triggers on branch pushes and pull requests only, so on a tag it
# does not race the release — it never runs at all. Without this job the boto3
# suite, the guardrail the project leans on hardest, has no bearing on what
# gets published.
compat:
needs: resolve
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.resolve.outputs.sha }}
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Install SQLite dev headers
run: sudo apt-get install -y libsqlite3-dev
- name: Build devcloud binary
run: CGO_ENABLED=1 go build -o dist/devcloud ./cmd/devcloud
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Install Python dependencies
run: pip install -r tests/compatibility/requirements.txt
- name: Run compatibility tests
run: pytest tests/compatibility/ -v --tb=short
env:
DEVCLOUD_BIN: dist/devcloud

release:
needs: [resolve, test, codegen-drift, compat]
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
attestations: write
steps:
# fetch-tags is not redundant with fetch-depth. actions/checkout always
# fetches with --no-tags and brings tags down only via an explicit refspec,
# and for a SHA ref that refspec is the bare commit — so pinning the commit
# without this leaves a repo with no tags at all, and GoReleaser cannot name
# the version it is releasing. With both, a tag that moved after `resolve`
# no longer points at HEAD and GoReleaser refuses the run outright.
- name: Checkout the code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ needs.resolve.outputs.sha }}
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Resolve tag
id: resolve_tag
- name: Verify changie release notes
run: |
echo "tag=${{ github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then
echo "extra_flags=--snapshot" >> "$GITHUB_OUTPUT"
notes="changes/${{ needs.resolve.outputs.tag }}.md"
if [ ! -f "$notes" ]; then
echo "::error::Changie fragment $notes not found."
echo "::error::Run 'changie batch ${{ needs.resolve.outputs.tag }}' and 'changie merge' before pushing the tag."
exit 1
fi
- name: Verify changie release notes exist
run: |
if [ ! -f "changes/${{ steps.resolve_tag.outputs.tag }}.md" ]; then
echo "::error::Changie fragment changes/${{ steps.resolve_tag.outputs.tag }}.md not found."
echo "::error::Run 'changie batch ${{ steps.resolve_tag.outputs.tag }}' and 'changie merge' before pushing the tag."
# changie renders every entry as '* ...', so a notes file with none of
# them was not batched from fragments. Checking that first stops the
# entry validation below from passing on an empty match set — a
# hand-written file (changes/v0.1.0.md is one) would otherwise sail
# through with nothing checked at all.
if ! grep -q '^\* ' "$notes"; then
echo "::error::$notes contains no changie entries. Release notes come from fragments — run 'changie batch ${{ needs.resolve.outputs.tag }}' and 'changie merge'."
exit 1
fi
# changie renders the heading for the version it batched, so a prefix
# match on '## ' accepts a file copied or renamed from an earlier
# release: the notes for this tag then open with a link to that other
# release, dated to it, and GoReleaser passes it through verbatim as
# the release body. Rebuild the heading .changie.yaml would have
# produced for this tag and require exactly that, exactly once.
tag="${{ needs.resolve.outputs.tag }}"
want="^## \[${tag//./\\.}\]\(https://github\.com/${{ github.repository }}/releases/tag/${tag//./\\.}\) - [0-9]{4}-[0-9]{2}-[0-9]{2}$"
if [ "$(grep -cE '^## ' "$notes")" != "1" ] || ! grep -qE "$want" "$notes"; then
grep -n '^## ' "$notes" || true
echo "::error::$notes needs exactly one changie version heading, naming $tag. Run 'changie batch $tag' instead of copying or renaming an earlier release's file."
exit 1
fi
# A batched file holds only what .changie.yaml renders: that heading,
# one kind heading per section, and one '* ...' entry per fragment.
# Allowing those three shapes and rejecting every other line catches
# the hand edits the entry check below cannot vouch for. Enumerating
# bullet markers instead misses everything that is not one — '1. text',
# an indented ' - text', a pasted paragraph — and each of those
# publishes with nothing about it checked.
#
# The kinds are read out of the config rather than restated here, so
# adding one to .changie.yaml does not start failing releases. An
# extraction that stops matching yields an empty set, which rejects
# every kind heading and fails loudly rather than checking nothing.
kinds="$(sed -n 's/^ - label: //p' .changie.yaml | paste -sd'|' -)"
foreign="$(grep -nvE "^$|^## |^### ($kinds)$|^\* " "$notes" || true)"
if [ -n "$foreign" ]; then
echo "$foreign"
echo "::error::The lines above are not changie output. Move them into changes/unreleased fragments and re-batch."
exit 1
fi
# 'changie batch' does not validate custom fields on hand-written
# fragments, so an Issue that is empty, zero or non-numeric renders a
# dead link and batches without complaint. Every entry must end in
# .../issues/<positive int>), so check the good form rather than
# enumerating the bad ones.
bad="$(grep '^\* ' "$notes" | grep -vE 'issues/[1-9][0-9]*\)\)$' || true)"
Comment thread
skyoo2003 marked this conversation as resolved.
if [ -n "$bad" ]; then
echo "$bad"
echo "::error::The entries above do not end in a valid issue link. Fix the Issue field in their changes/unreleased fragment, then re-batch."
exit 1
fi
- name: Set up Docker Buildx
Expand Down Expand Up @@ -79,7 +254,7 @@ jobs:
with:
distribution: goreleaser
version: "~> v2"
args: release --clean ${{ steps.resolve_tag.outputs.extra_flags }} --release-notes changes/${{ steps.resolve_tag.outputs.tag }}.md
args: release --clean ${{ needs.resolve.outputs.extra_flags }} --release-notes changes/${{ needs.resolve.outputs.tag }}.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Always defined: {{ .Env.HOMEBREW_TAP_TOKEN }} fails to render if the
Expand Down
16 changes: 16 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ archives:
- LICENSE
- README.md
- CHANGELOG.md
# Docs are versioned by tag, and the ones stating what a release promises —
# the fidelity manifest, the compatibility policy — are only true of the
# binary they ship beside.
- docs
Comment thread
skyoo2003 marked this conversation as resolved.
# README.md and the docs tree reach these by relative path, so leaving them
# out dangles the archive's own entry point: before docs/ was added here,
# 26 of README.md's 28 relative links resolved to nothing. What stays broken
# points into source and CI config, which a binary archive has no business
# carrying.
- CONTRIBUTING.md
- CODE_OF_CONDUCT.md
- GOVERNANCE.md
- SECURITY.md
- SUPPORT.md
- NOTICE
- TRADEMARKS.md

changelog:
disable: false
Expand Down
2 changes: 1 addition & 1 deletion changes/unreleased/Added-20260809-120000.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Added
body: Per-operation fidelity manifest declaring every operation as hand-verified, auto-crud or unimplemented, exposed at `GET /devcloud/api/fidelity` and enforced by a build-failing coverage test
time: 2026-08-09T12:00:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Added-20260809-140000.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Added
body: Real tag support for KMS, CloudWatch and EventBridge — TagResource, UntagResource and ListTagsForResource/ListResourceTags now persist tags per resource ARN instead of being echoed by the generic CRUD engine
time: 2026-08-09T14:00:00.000000+09:00
custom:
Issue: ""
Issue: "126"
5 changes: 5 additions & 0 deletions changes/unreleased/Changed-20260809-180000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Changed
body: Release archives now carry the `docs/` tree alongside the binary, so the documentation you unpack — including the fidelity manifest and the release's compatibility promises — describes exactly the version you downloaded
time: 2026-08-09T18:00:00.000000+09:00
custom:
Issue: "127"
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
kind: Fixed
body: Config-time warnings (deprecated `dashboard` key, unknown `DEVCLOUD_SERVICES` tier) now honor `logging.format`/`logging.level` instead of always printing as plain text before the logger is configured
time: 2026-07-25T01:07:55.000000+09:00
custom:
Issue: "113"
Issue: "113"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-120100.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: Codegen now parses operations bound to Smithy resource shapes, recovering 285 operations that were invisible to the generator (bedrock 0 of 101, lambda 19 of 85, ecs 12 of 76, transfer 29 of 71, sso-admin 67 of 79)
time: 2026-08-09T12:01:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-140100.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: The fidelity manifest now reads each provider's actual dispatch instead of intersecting with the Smithy model, recovering 226 served operations it had hidden (dynamodbstreams listed 4 of its 22, acm's UpdateCertificate, bedrock's InvokeModelWithResponseStream) and dropping 5 non-operations it had invented (identitystore Description/DisplayName/Emails, pipes DELETE/POST)
time: 2026-08-09T14:01:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-160000.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: EventBridge now drops a bus's or rule's tags when it is deleted. ARNs are derived from the name, so recreating a deleted resource reused its ARN and inherited the previous tags
time: 2026-08-09T16:00:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-160100.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: Restored CloudWatch's 17 CRUD-engine operations. The gateway picks the protocol from the request, not from the provider, so CloudWatch reaches the engine whenever a client speaks JSON — filtering the registry by the provider's declared protocol had removed that coverage outright
time: 2026-08-09T16:01:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-170000.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: EventBridge rule ARNs now name their event bus, as AWS does. A rule name is unique per bus, so same-named rules on two custom buses previously shared one ARN — and with it, one tag set, where tagging one rule changed the other's and deleting one wiped the survivor's
time: 2026-08-09T17:00:00.000000+09:00
custom:
Issue: ""
Issue: "126"
2 changes: 1 addition & 1 deletion changes/unreleased/Fixed-20260809-170100.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ kind: Fixed
body: CloudWatch now drops an alarm's tags when the alarm is deleted, so recreating an alarm under the same name no longer inherits the old one's tags
time: 2026-08-09T17:01:00.000000+09:00
custom:
Issue: ""
Issue: "126"
15 changes: 15 additions & 0 deletions cmd/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func main() {

var crudServices []codegen.CRUDServiceData
modelOps := make(map[string][]string)
// A model that cannot be read or parsed is skipped, which used to leave the
// exit status at 0 — so a drift check downstream saw no changed files and
// called incomplete generation clean.
skipped := false

for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
Expand All @@ -56,12 +60,14 @@ func main() {
data, err := os.ReadFile(filepath.Join(*modelsDir, entry.Name()))
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", entry.Name(), err)
skipped = true
continue
}

model, err := codegen.ParseSmithyJSON(data)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", entry.Name(), err)
skipped = true
continue
}

Expand Down Expand Up @@ -94,6 +100,15 @@ func main() {
}
}

// Bail before the aggregate artefacts: the CRUD registry and the fidelity
// manifest describe the whole fleet, and writing them from a set that is
// missing a service would state, in generated code, that its operations do
// not exist.
if skipped {
fmt.Fprintln(os.Stderr, "Error: one or more models were skipped; generated output is incomplete")
os.Exit(1)
}

// Write the aggregate CRUD registry only when generating the full fleet
// (a filtered run would otherwise clobber it with a partial registry).
if len(allowedServices) == 0 {
Expand Down
Loading